Любознательный сотрудник Яндекс Почты Амина задала...
Criado em: 18 de fevereiro de 2025
Criado em: 18 de fevereiro de 2025
Любознательный сотрудник Яндекс Почты Амина задалась вопросом, а кто из пользователей сервиса общается активнее всех. Из-за большой загруженности на работе Амина так и не смогла найти время для решения этой задачи и поэтому просит вас ей помочь.
Вам дана таблица отправленных сообщений в Яндекс Почте:
messages (
message_id INTEGER NOT NULL,
sender_id INTEGER NOT NULL,
receiver_id INTEGER NOT NULL,
reply_message_id INTEGER
);
Где message_id — идентификатор отправленного сообщения (уникален в рамках общения любых двух пользователей), sender_id — идентификатор отправителя сообщения, receiver_id — идентификатор получателя сообщения (отличается от sender_id), reply_message_id — идентификатор сообщения, для которого данное сообщение является ответом (опциональное поле). Будем называть тредом такую цепочку из сообщений, где каждое последующее сообщение является ответом на предыдущее. В рамках одного треда сообщениями обмениваются только 2 пользователя. При этом у одного пользователя может быть несколько тредов как с разными пользователями, так и с одним. Для первого сообщения в треде reply_message_id пустой и равняется null. Рассмотрим пример:
message_id sender_id receiver_id reply_message_id
1 1 2 null
3 2 1 1
19 3 4 16
16 4 3 1
1 3 4 null
4 1 2 3
7 3 4 null
17 4 3 7
Пользователи
Unexpected text node: '
'
1 и
Unexpected text node: '
'
2 общались в рамках одного треда, который можно представить в виде цепочки из message_id:
Unexpected text node: '
'
1→3→4. Пользователи
Unexpected text node: '
'
3 и
Unexpected text node: '
'
4 общались в рамках двух тредов, которые также можно представить в виде цепочек из message_id:
Unexpected text node: '
'
1→16→19 и
Unexpected text node: '
'
7→17.
Вашей задачей является создание запроса на SQLite, который для каждого пользователя посчитает максимальную длину треда, в котором он принимал участие (длиной треда является количество сообщений в нем). Результатом должна быть таблица с двумя колонками: user_id (идентификатор пользователя из sender_id/receiver_id) и max_thread_messages (максимальная длина треда для данного пользователя). Выходная таблица должна быть отсортирована по убыванию max_thread_messages (в случае равенства max_thread_messages предпочтение должно отдаваться пользователю с наименьшим user_id).
Для примера выше ответом будет следующая таблица:
user_id max_thread_messages
1 3
2 3
3 3
4 3
Примечания
Гарантируется, что одному сообщению может соответствовать только один ответ и для каждого непустого значения reply_message_id в таблице присутствует сообщение с соответствующим message_id в рамках определенной пары пользователей.
Ниже приведён один из вариантов решения на чистом SQL (SQLite) с использованием рекурсивного CTE. Основная идея:
reply_message_id IS NULL
. Это начало каждой цепочки.MAX(depth)
– «самая дальняя» глубина в рекурсивной цепочке.MAX
по всем тредам для каждого пользователя.user_id
.Пример запроса (названия столбцов и таблицы можно при необходимости подстроить под свои):
sqlWITH RECURSIVE chain AS ( -- 1) Берём "корневые" сообщения, у которых нет reply_message_id. SELECT message_id, sender_id, receiver_id, reply_message_id, message_id AS root_id, -- идентификатор «корня» (начала) треда 1 AS depth -- начальная глубина FROM messages WHERE reply_message_id IS NULL UNION ALL -- 2) Рекурсивно цепляем все ответы, которые продолжают ту же пару пользователей SELECT m.message_id, m.sender_id, m.receiver_id, m.reply_message_id, c.root_id, c.depth + 1 FROM messages m JOIN chain c ON m.reply_message_id = c.message_id -- Один и тот же тред продолжается только у той же пары пользователей, -- вне зависимости от того, кто отправляет/получает. AND ( (m.sender_id = c.sender_id AND m.receiver_id = c.receiver_id) OR (m.sender_id = c.receiver_id AND m.receiver_id = c.sender_id) ) ), -- 3) Находим длину каждого треда (максимальную глубину для каждого root_id). thread_length AS ( SELECT root_id, MAX(depth) AS len FROM chain GROUP BY root_id ), -- 4) "Раздаём" длину треда каждому пользователю, который был в этом треде. user_thread AS ( SELECT sender_id AS user_id, root_id FROM chain UNION SELECT receiver_id AS user_id, root_id FROM chain ), -- Для каждого пользователя берём максимум из всех тредов, в которых он участвовал. user_max_thread AS ( SELECT user_id, MAX(len) AS max_thread_messages FROM user_thread JOIN thread_length USING (root_id) GROUP BY user_id ) -- 5) Итоговая выборка с нужной сортировкой SELECT user_id, max_thread_messages FROM user_max_thread ORDER BY max_thread_messages DESC, user_id ASC;
CTE chain
SELECT ... FROM messages WHERE reply_message_id IS NULL
забирает все «начальные» сообщения тредов (где нет ответа на предыдущее). Каждому такому сообщению присваиваем root_id = message_id
и depth = 1
.UNION ALL
рекурсивно ищет сообщения, у которых reply_message_id
совпадает с message_id
из уже взятой строки. При этом мы проверяем, что новое сообщение идёт в общении тех же двух пользователей (порядок sender_id
/receiver_id
может меняться).CTE thread_length
Для каждой цепочки (идентифицируемой полем root_id
) считаем максимальную глубину depth
. Это и есть длина данного треда.
CTE user_thread
Каждый пользователь в цепочке может быть либо отправителем (sender_id
), либо получателем (receiver_id
). Чтобы учесть оба случая, делаем UNION
и выписываем (пользователь, номер треда).
CTE user_max_thread
Объединяем полученный набор (user_id, root_id)
с длинами тредов (по root_id
), берём MAX(len)
– максимальную длину треда, в котором данный пользователь участвовал.
Итоговый SELECT
Выбираем (user_id, max_thread_messages)
и сортируем по убыванию длины треда, а при равенстве – по возрастанию идентификатора пользователя.
Такой запрос вернёт именно ту структуру, которую просит задача: для каждого пользователя (участвующего в таблице messages
) — максимальную длину треда и нужную сортировку на выходе.