SQL - схема сообщения - необходимо найти существующий поток сообщений с учетом набора пользователей

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

Возможны 2 варианта отправки сообщения:

Отправить в ветку. При просмотре ветки сообщение отправляется непосредственно в эту ветку, поэтому идентификатор ветки известен. (не проблема)

Отправить получателям. Пользователь создает новое сообщение и указывает набор получателей с нуля. Я хочу создать новый поток только в том случае, если он еще не существует между этими пользователями, и именно здесь я застрял. Мне нужен запрос, который найдет существующий идентификатор потока с учетом набора пользователей. Таблица ThreadMembers сопоставляет пользователей с потоками. Это вообще возможно? Или мне нужно изменить мои таблицы?

Мои столы:

Поток:
идентификатор потока (идентификатор)
lastSent (отметка времени)

ThreadMembers:
threadFK (внешний ключ для потока)
userFK (внешний ключ для пользователя)

Сообщения:
threadFK (внешний ключ для потока)
senderFK (внешний ключ для пользователя)
msgID (идентификатор)
msgDate (отметка времени)
msgText (текст)

Большое спасибо!


person Redtopia    schedule 28.03.2012    source источник
comment
Может быть что-то вроде: SELECT threadFK FROM ThreadMembers WHERE ‹threadFK тот же самый› и userFK IN (user1,user2,user3,user4)   -  person Redtopia    schedule 28.03.2012
comment
Какая СУБД? (Oracle? PostgreSQL? MySQL? SQL Server?)   -  person ruakh    schedule 28.03.2012
comment
Кстати, означает ли этот подход, что каждое отдельное сообщение от пользователя А к пользователю Б или наоборот будет принадлежать одному потоку, даже если эти сообщения охватывают несколько лет? Это немного нелогично.   -  person ruakh    schedule 28.03.2012
comment
SQL Server... и да, поток будет длиться вечно. Одна цепочка свяжет все сообщения вместе для любой группы получателей, будь то 2 человека или 200 человек.   -  person Redtopia    schedule 28.03.2012


Ответы (4)


ИЗМЕНИТЬ:

Я понял, пытаясь объяснить запрос, что он не всегда будет работать правильно. Итак, я вернулся и понял, как это проверить. Меня все еще беспокоит настройка схемы, а именно, она подразумевает, что новые пользователи не могут быть добавлены в существующий поток, и что определенный набор пользователей сможет общаться только в одном потоке, но это было хорошо, чтобы исправить запрос.

WITH Selected_Users(id) as (VALUES (@id1), (@id2), --etc--),
     Threads(id) as (SELECT DISTINCT threadFk
                     FROM ThreadMembers as a
                     JOIN Selected_Users as b
                     ON b.id = a.userFk)
SELECT a.id
FROM Threads as a
WHERE NOT EXISTS (SELECT '1'
                  FROM ThreadMembers as b
                  LEFT JOIN Selected_Users as c
                  ON c.id = b.userFk
                  WHERE c.id IS NULL
                  AND b.threadFk = a.id)
AND NOT EXISTS (SELECT '1'
                FROM Selected_Users as b
                LEFT JOIN ThreadMembers as c
                ON c.userFk = b.id
                AND c.threadFk = a.id
                WHERE c.userFk IS NULL) 

Оператор, скорее всего, должен быть динамическим, чтобы построить список выбранных пользователей, если только SQL Server не имеет способа предоставить список в качестве переменной хоста (я знаю, что DB2 делает это, по крайней мере, из iSeries). У меня нет идеального набора данных для проверки этого, но для таблицы с несколькими миллионами строк (только с отношением «многие-один») он возвращается почти мгновенно — для этого я получаю доступ только к индексу (подсказка) .

Пояснения:

WITH Selected_Users(id) as (VALUES (@id1), (@id2), --etc--),

Этот CTE строит список пользователей, чтобы на него можно было ссылаться как на таблицу. Это упрощает работу, хотя можно было бы просто везде заменить его оператором IN (хотя требуется несколько ссылок).

     Threads(id) as (SELECT DISTINCT threadFk
                     FROM ThreadMembers as a
                     JOIN Selected_Users as b
                     ON b.id = a.userFk)

Этот CTE получает список (отдельных) потоков, в которых участвуют пользователи. В основном это просто сокращение списка до отдельных ссылок на threadFk.

SELECT a.id
FROM Threads as a

... Получить выбранный набор потоков...

WHERE NOT EXISTS (SELECT '1'
                  FROM ThreadMembers as b
                  LEFT JOIN Selected_Users as c
                  ON c.id = b.userFk
                  WHERE c.id IS NULL
                  AND b.threadFk = a.id)

Где нет никого, кто «пропал без вести» из выбранного списка пользователей, то есть он исключает потоки со списками пользователей, которые являются подмножествами большего списка. Кроме того, исключаются потоки, в которых есть некоторые пользователи, перечисленные в списке, а также несколько, которые не входят в список, а это означает, что количество пользователей будет совпадать, а фактические пользователи — нет (это где моя первая версия не удалась).


РЕДАКТИРОВАНИЕ:

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

AND NOT EXISTS (SELECT '1'
                FROM Selected_Users as b
                LEFT JOIN ThreadMembers as c
                ON c.userFk = b.id
                AND c.threadFk = a.id
                WHERE c.userFk IS NULL) 

Этот пункт исправляет это. Это гарантирует, что в списке выбора не останется никаких оставшихся пользователей после исключения пользователей для определенного потока.

Утверждение теперь меня немного беспокоит - для меня может быть немного лучший способ сделать это...


РЕДАКТИРОВАНИЕ:

Мувахаха, есть есть версия COUNT(*), которая тоже должна быть быстрее:

WITH Selected_Users(id) as (VALUES (@id1), (@id2), --etc--),
SELECT a.threadFk
FROM ThreadMembers as a
JOIN Selected_Users as b
ON b.id = a.userFk
GROUP BY a.threadFk
HAVING COUNT(*) = (SELECT COUNT(*) FROM Selected_Users)
AND COUNT(*) = (SELECT COUNT(*) from ThreadMembers as c
                WHERE c.threadFk = a.threadFk)

Пояснения:

SELECT a.threadFk
FROM ThreadMembers as a
JOIN Selected_Users as b
ON b.id = a.userFk

Это объединение, чтобы получить все потоки, частью которых являются перечисленные участники. Это внутренний эквивалент Threads CTE выше. На самом деле вы можете удалить этот CTE и в приведенном выше запросе.

GROUP BY a.threadFk

В конце концов, нам нужен только один экземпляр данного потока. Кроме того (по крайней мере, в DB2) остальная часть оператора недействительна, если она не присутствует.

HAVING COUNT(*) = (SELECT COUNT(*) FROM Selected_Users)

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

AND COUNT(*) = (SELECT COUNT(*) from ThreadMembers as c
                WHERE c.threadFk = a.threadFk)

Убедитесь, что для данного потока нет невыбранных пользователей. Или не должно быть никаких пользователей, «не учтенных»

Для этого вы должны получить доступ только к индексу (кажется, да). COUNT(*) строк результатов (для GROUP BY) следует выполнять только один раз и использовать повторно. Предложение HAVING оценивается после выполнения GROUP BY (если я правильно помню), поэтому подвыбор количества из исходной таблицы должен выполняться только один раз за threadFk .

person Clockwork-Muse    schedule 28.03.2012
comment
Спасибо X-Zero! Я пытаюсь разобраться с запросом... хотя я написал много SQL, я ни в коем случае не эксперт, и такие запросы меня сбивают с толку. Можете ли вы добавить комментарий, который кратко поясняет, что делает запрос? И предвидите ли вы какие-либо проблемы с производительностью, если будут миллионы потоков? - person Redtopia; 28.03.2012
comment
Поток определяется как диалог между группой пользователей. Я не думал о добавлении новых пользователей в ветку, но почему бы и нет? Вроде хорошая функция. Я собираюсь попробовать это, но я не уверен, куда это меня приведет. Тем не менее, я не смогу добраться до этого в течение нескольких дней... а пока я даю вам балл за вашу помощь! - person Redtopia; 28.03.2012
comment
Я пытаюсь написать этот запрос в SQL Server 2008 и получаю некоторые ошибки. Первая часть строит таблицу из идентификаторов пользователей, это правильно? Я считаю, что синтаксис в TSQL отличается. У вас есть идеи, как написать это на TSQL? - person Redtopia; 29.03.2012
comment
Да, первая часть — это просто получение таблицы пользователей. Извините, я действительно не знаю. Я думал, что ваши хост-переменные обозначаются '@', но могу ошибаться. Если вы создаете его динамически, вы должны просто указать идентификаторы. Именно этот оператор (ну, с заменой переменных хоста) работал в моей системе (DB2), и я думал, что он будет универсальным. - person Clockwork-Muse; 29.03.2012
comment
Хорошо, я думаю, что у меня есть временная таблица, но у меня проблемы с именами столбцов. Вторая строка: Threads(id)... id не является полем в Threads... разве это не должно быть Threads(threadID)? - person Redtopia; 29.03.2012
comment
Получилось... вроде работает. Я разместил версию TSQL ниже. Спасибо! - person Redtopia; 29.03.2012
comment
Извините, я пропустил, что назвал один из своих CTE так же, как и таблицу, - это вызовет коллизии. Я рекомендую вам использовать (или хотя бы посмотреть) версию COUNT(*), которую я разместил внизу - она ​​может быть быстрее и не имеет проблемы с коллизиями. - person Clockwork-Muse; 29.03.2012

Я не рекомендую это легкомысленно, но я думаю, что вам лучше немного денормализировать, добавив столбец в Thread, который содержит отсортированный список внешних ключей, разделенных запятыми, в User. И индексация этого столбца. Затем ваше приложение просто должно отсортировать идентификаторы пользователей отправителя + всех получателей, соединить отсортированный список запятыми и найти запись Thread.

Поскольку по определению список пользователей в потоке никогда не меняется, вам просто нужно правильно заполнить эти вещи при вставке, и вам не нужно беспокоиться о согласованности последующих обновлений.

(Для ясности: то, что вы описываете, определенно возможно с правильно нормализованной схемой. Но это будет уродливо, и я думаю, что это будет плохо работать.)

person ruakh    schedule 28.03.2012
comment
Интересный подход... Я стараюсь избегать полей, содержащих значения, разделенные запятыми, но вижу, как это может быть эффективно. - person Redtopia; 28.03.2012
comment
Подумав об этом, я не уверен, что это будет легко, эффективно или менее уродливо. Что делать, если в вашей теме 100 получателей? Поле получателей будет длинным списком, разделенным запятыми. Вам нужно будет убедиться, что каждый получатель будет в этом списке, что (я думаю) было бы очень некрасиво. - person Redtopia; 28.03.2012
comment
@Redtopia: я не знаю, что вы имеете в виду под «Вы должны убедиться, что каждый получатель будет в этом списке — вы говорите, что два сообщения принадлежат одному и тому же потоку, если их списки пользователей точно < /i> то же самое, так что это всего лишь вопрос простого равенства строк (WHERE comma_separated_sorted_list = '...'). Если вас беспокоит длина, альтернативой является сохранение хеша MD5 или SHA-1 списка, а не самого списка, или сохранение такого хэша в дополнение к первым 100 символам списка или что-то еще. - person ruakh; 28.03.2012
comment
Итак, список нужно отсортировать. В этом есть смысл. - person Redtopia; 28.03.2012

Правильно ли сказать, что вас интересует, существует ли какой-либо поток, который: 1) имеет такое же количество участников в потоке при группировании по threadFK, что и количество членов интересующей вас группы, 2) имеет и ссылается на каждого члена? Если это так, я думаю, что оттуда последует решение (так что это предлагаемый ответ). Точная механика будет зависеть от того, какую базу данных вы используете, oracle, postgres или sql server, вероятно, будет проще, чем другие бренды. Как вы хотите назвать эту вещь, как хранимую процедуру, которая берет таблицу пользователей, список имен пользователей и возвращает, что, ключ, если есть совпадение, или NULL?

person Levin Magruder    schedule 28.03.2012
comment
Будет несколько потоков с одинаковым количеством пользователей, поэтому я не уверен, что это поможет. Кажется, что это должно быть просто, хотя, как и любая система обмена сообщениями, она также должна быть эффективной. Учитывая набор пользователей, мне нужно увидеть, существует ли между ними поток. Если нет, то я создам его. Между любой группой пользователей должен быть только один поток. Предположим, будут миллионы потоков. Я еще ничего не реализовал, поэтому ответ может предложить изменения в моих таблицах. - person Redtopia; 28.03.2012

Вот пример ответа (номер ответа 1) с использованием MS SQL Server 2008. Это предполагает, что таблица: MessageThreadUsers (threadFK - int, userFK - varchar) определена (ваши типы ключей могут быть другими):

DELETE FROM MessageThreadUsers
GO

INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (1, 'user1')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (1, 'user2')

INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (2, 'user1')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (2, 'user2')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (2, 'user3')

INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (3, 'user1')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (3, 'user2')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (3, 'user3')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (3, 'user4')

INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (4, 'user1')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (4, 'user2')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (4, 'user3')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (4, 'user4')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (4, 'user5')

INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (5, 'user1')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (5, 'user2')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (5, 'user3')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (5, 'user4')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (5, 'user5')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (5, 'user6')

INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (6, 'user6')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (6, 'user3')
INSERT INTO MessageThreadUsers (threadFK, userFK) VALUES (6, 'user1')

GO

WITH Selected_Users (id) AS (
    SELECT 'user3' UNION
    SELECT 'user1' UNION
    SELECT 'user6'
)
SELECT a.threadFk
FROM MessageThreadUsers as a
JOIN Selected_Users as b
ON b.id = a.userFk
GROUP BY a.threadFk
HAVING COUNT(*) = (SELECT COUNT(*) FROM Selected_Users)
AND COUNT(*) = (SELECT COUNT(*) from MessageThreadUsers as c
                WHERE c.threadFk = a.threadFk)
person Redtopia    schedule 28.03.2012