Выбор sql из таблицы «один ко многим»

У меня есть 3 таблицы с этими столбцами ниже:

Topics:
[TopicID] [TopicName]
Messages:
[MessageID] [MessageText]
MessageTopicRelations
[EntryID] [MessageID] [TopicID]

сообщения могут быть более чем на одну тему. вопрос: учитывая пару тем, мне нужно получать сообщения, которые касаются ВСЕХ этих тем и не меньше, но они могут быть и о какой-то другой теме. сообщение, касающееся НЕКОТОРЫХ из этих заданных тем, не будет включено. Надеюсь, я хорошо объяснил свою просьбу. в противном случае я могу предоставить образцы данных. спасибо


person taz    schedule 03.01.2010    source источник
comment
просто чтобы внести ясность, ваша схема выглядит как «многие ко многим», а не «один ко многим»...?   -  person Funka    schedule 03.01.2010


Ответы (4)


Далее используются x, y и z для обозначения идентификаторов тем, поскольку они не были предоставлены для примеров.

Использование СОЕДИНЕНИЙ:

SELECT m.*
  FROM MESSAGES m
  JOIN MESSAGETOPICRELATIONS mtr ON mtr.messageid = m.messageid
  JOIN TOPICS tx ON tx.topicid = mtr.topicid
                AND tx.topicid = x
  JOIN TOPICS ty ON ty.topicid = mtr.topicid
                AND ty.topicid = y
  JOIN TOPICS tz ON tz.topicid = mtr.topicid
                AND tz.topicid = z

Использование GROUP BY/HAVING COUNT(*):

  SELECT m.*
    FROM MESSAGES m
    JOIN MESSAGETOPICRELATIONS mtr ON mtr.messageid = m.messageid
    JOIN TOPICS t ON t.topicid = mtr.topicid
   WHERE t.topicid IN (x, y, z)
GROUP BY m.messageid, m.messagetext
  HAVING COUNT(*) = 3

Из этих двух подход JOIN более безопасен.

GROUP BY/HAVING полагается на то, что MESSAGETOPICRELATIONS.TOPICID либо является частью первичного ключа, либо имеет уникальное ограничение ключа, чтобы гарантировать отсутствие дубликатов. В противном случае у вас может быть более 2 экземпляров одной и той же темы, связанных с сообщением, что будет ложным срабатыванием. Использование HAVING COUNT(DISTINCT ... устраняет любые ложные срабатывания, но поддержка зависит от базы данных — MySQL поддерживает ее в 5.1+, но не в 4.1. Oracle, возможно, придется подождать до понедельника, чтобы протестировать SQL Server...

Я просмотрел комментарий Билла о том, что не нужно присоединение к таблице TOPICS:

SELECT m.*
  FROM MESSAGES m
  JOIN MESSAGETOPICRELATIONS mtr ON mtr.messageid = m.messageid
                                AND mtr.topicid IN (x, y, z)

... вернет ложные срабатывания - строки, которые соответствуют хотя бы одному из значений, определенных в предложении IN. И:

SELECT m.*
  FROM MESSAGES m
  JOIN MESSAGETOPICRELATIONS mtr ON mtr.messageid = m.messageid
                                AND mtr.topicid = x
                                AND mtr.topicid = y
                                AND mtr.topicid = z

... вообще ничего не вернет, потому что topicid никогда не может быть всеми значениями сразу.

person OMG Ponies    schedule 03.01.2010
comment
Вам не нужно присоединение к таблице тем, только к таблице MessageTopicRelations. И я бы предложил COUNT(DISTINCT topicid) = 3, если вы не можете полагаться на уникальное ограничение в таблице MessageTopicRelations поверх (messageid, themeid). - person Bill Karwin; 03.01.2010
comment
@Bill: В прошлый раз, когда я проверял, HAVING COUNT(DISTINCT ... не работал в MySQL. Не знаю, для чего это дб на этой ноте - person OMG Ponies; 03.01.2010
comment
Каждая таблица «многие ко многим», которую я когда-либо реализовывал или с которой сталкивался, имела составной первичный ключ, поэтому я думаю, что предупреждение для большинства схем является спорным. - person Paul Creasey; 03.01.2010
comment
@Paul: ORM не любят составные ключи IIRC, и по SO достаточно вопросов, чтобы я никогда не предполагал наличие уникального ключа, а тем более первичного ключа:/ - person OMG Ponies; 03.01.2010
comment
Хммм, достаточно честно, я полагаю, что предположение делает из нас с вами задницу, особенно на SO. Насколько я знаю, в наши дни большинство ORM хорошо обрабатывают составные ключи, по крайней мере те, которые я использовал! :) - person Paul Creasey; 03.01.2010

Вот глубоко неэлегантное решение

SELECT
     m.MessageID
    ,m.MessageText
FROM
    Messages m
WHERE
    m.MessageID IN (
    SELECT
        mt.MessageID
    FROM
        MessageTopicRelations mt
    WHERE
        TopicID IN (1,4,5)// List of topic IDS
    GROUP BY
        mt.MessageID
    HAVING
        count(*) = 3 //Number of topics
    )
person Paul Creasey    schedule 03.01.2010
comment
«Глубоко неэлегантно», забавно :-) Однако я не верю, что это сработает, поскольку count(*) может равняться трем разными способами, кроме одного, которого требует OP. - person mechanical_meat; 03.01.2010
comment
@Adam: Ваша озабоченность верна - решение зависит от модели данных. Подробности смотрите в моем ответе, но +1 от меня. - person OMG Ponies; 03.01.2010
comment
Я предполагаю, что таблица «многие ко многим» использует составной ключ, это то, что я считаю типичной и лучшей практикой. Если это не так, то запрос недействителен. - person Paul Creasey; 03.01.2010
comment
@Paul: правильно понял. Я думаю, это делает вас оптимистом, а меня пессимистом ;-) - person mechanical_meat; 03.01.2010

Редактировать: спасибо @Paul Creasey и @OMG Ponies за то, что нашли недостатки в моем подходе.
Правильный способ сделать это — самостоятельно присоединиться к каждой теме; как показано в ведущем ответе.


Еще одна крайне неэлегантная запись:

select m.MessageText
       , t.TopicName
  from Messages m
       inner join MessageTopicRelations mtr
       on mtr.MessageID = m.MessageID
       inner join Topics t
       on t.TopicID = mtr.TopicID
   and
       t.TopicName = 'topic1'

UNION 

select m.MessageText
       , t.TopicName
  from Messages m
       inner join MessageTopicRelations mtr
       on mtr.MessageID = m.MessageID
       inner join Topics t
       on t.TopicID = mtr.TopicID
   and
       t.TopicName = 'topic2'
...
person mechanical_meat    schedule 03.01.2010
comment
Попутно: этот тип запроса чрезвычайно и элегантно прост при запросе к ListProperty в хранилище данных Google App Engine. - person mechanical_meat; 03.01.2010
comment
+1: это тоже сработает, хотя требует больше ввода, чем методы JOINs или GROUP BY/HAVING COUNT. - person OMG Ponies; 03.01.2010
comment
Я не понимаю, я что-то пропустил? Это не решает проблему и не имеет смысла для меня. Он наверняка вернет любое сообщение с любой темой и зачем союз, когда вы можете использовать ИЛИ или В - person Paul Creasey; 03.01.2010
comment
@Paul: Теперь, когда я перечитал его, да, он вернет результаты, эквивалентные использованию IN (1, 2, 3), потому что UNION не гарантирует, что возвращаемое сообщение относится к обеим темам. - person OMG Ponies; 03.01.2010

Re: ответ OMG Ponies, вам не нужно присоединяться к таблице TOPICS. И предложение HAVING COUNT(DISTINCT) отлично работает в MySQL 5.1. Я только что проверил это.

Это то, что я имею в виду:

Использование GROUP BY/HAVING COUNT(*):

  SELECT m.*
    FROM MESSAGES m
    JOIN MESSAGETOPICRELATIONS mtr ON mtr.messageid = m.messageid
   WHERE mtr.topicid IN (x, y, z)
GROUP BY m.messageid
  HAVING COUNT(DISTINCT mtr.topicid) = 3

Причина, по которой я предлагаю COUNT(DISTINCT), заключается в том, что если столбцы (messageid,topicid) не имеют уникального ограничения, вы можете получить дубликаты, что приведет к количеству 3 в группе, даже с менее чем тремя различными значениями.

person Community    schedule 04.01.2010
comment
@Bill: Я понял, что это единственный способ, о котором вы говорили, - это должно было быть с HAVING COUNT(DISTINCT... (что у меня не работало на 4.1). Спасибо - ценю информацию! - person OMG Ponies; 04.01.2010
comment
MySQL 4.1 существует более пяти лет, и его расширенная поддержка официально закончилась. Я думаю, что разумно отвечать на вопросы StackOverflow, основываясь на активно поддерживаемой версии 5.1. - person Bill Karwin; 04.01.2010
comment
Я поддерживаю приложение, использующее 4.1 - я все о старой школе =) - person OMG Ponies; 04.01.2010