Nick
Nalik
Nalik
А понял
Nalik
Да как вариант
Nalik
Я просто пытаюсь исключить на будущее потенциально узкие и ресурсоемкие места и сразу сделать все правильно 😄
Nalik
навелосипедить это всегда пожалуйста)
Nick
Как ты себе это представляешь, когда тысячи юзеров?
я просто считаю, что тыщи юзеров как правило ни у кого не бывает, а тем более в одном чате. А у кого бывает они проектируют решения по другому. Ну и такие операции разовые при подгрузке событий и нагрузки не дают особо, а остальное реалтаймово на событиях и не приводит к доп запросам.
Nalik
Алексей
любой мессенджер начинается ровно с 1 юзера
Nalik
Я думаю если что-то делать, то нужно заранее предусмотреть и по возможности исключить слабые места)
Nalik
Nalik
Окей, другой вопрос. Так же по архитектуре. Как сделать подсчет непрочитанных сообщений в чатах (как это реализовано допустим в том же Telegram).
Допустим есть коллекции:
chats_list - Здесь хранится информация обо всех чатах.
chats_user - Здесь хранится информация о чатах в которых состоит пользователь
messages_list - Здесь хранится информация обо всех сообщениях в чатах
Сейчас у меня есть такая идея
1. В chats_list хранить ID последнего сообщения чата.
2. В chats_user хранить ID последнего прочитанного сообщения чата.
3. Делать выборку из messages_list сообщений созданных после "ID последнего сообщения чата"
Но этот способ слишком ресурсоемкий, особенно когда много сообщений в коллекции messages_list
Алексей
после первого транша от инвестора!
Nick
Nalik
Есть и другая идея:
Можно добавлять локальный msg_local_id сообщения в таблицу messages_list, в таблице chats_list запоминать msg_last_local_id последнего локального сообщения (он же будет идентификатором сколько сообщений всего) и при добавлении нового сообщения этот счетчик будет увеличиваться по типу autoincrement, в таблицу chats_user заносить msg_read_local_id последнего прочитанного сообщения. И когда нужно будет посчитать количество непрочитанных сообщений просто отнимаем от msg_last_local_id значение из msg_read_local_id. Такой запрос не будет потреблять ресурсы.
Nalik
Но мне кажется муторная схема
Алексей
Но мне кажется муторная схема
позвольте поинтересоваться -вы на клиента какую то логику возлагаете или все должно на бекенде и монге решаться?
Nalik
Алексей
ну хотя бы тема оформления не будет в коллекции храниться?))
Алексей
а на чем бек пишете?
Nalik
Не, вообще мне кажется это очевидно должно вычисляться на бэке.
Взять хотя бы API VK https://dev.vk.com/ru/reference/objects/conversation
Nalik
значение unread_count
Nalik
Nalik
Да определенно данные будут кэшироваться, вся возможная логика так же будет разгружена на клиенте, чтобы не нагружать сервер. Но сервер тем не менее должен знать все данные так или иначе чтобы иметь возможность разово отправить все данные на клиент не сильно потея. Вот сейчас я войду с нового устройства в телегу и она должна будет отправить мне все данные, а на это уйдет куда больше вычислительных ресурсов, чем при входе с устройства которым я постоянно пользуюсь.
Nalik
Как минимум телега должна будет мне отправить список чатов в которых я состою с доп.ифнормацией типа последнего сообщения и счетчика непрочитанных сообщений)
Nalik
@yatoba кстати по поводу вчерашней темы. group не подходит потому, что я сначала выбираю список своих чатов.
await chats_user.aggregate([
{ $match: { user_id: 1 } },
{ $group: { _id: "$chat_id", count: { $sum: 1 } } },
]);
Если бы мне нужно было подсчитать инфу во всех чатах или в каких-то конкретных, то все было бы ок.
await chats_user.aggregate([
{ $group: { _id: "$chat_id", count: { $sum: 1 } } },
]);
Nick
Nalik
Nick
Да все есть
значит
$match: {chat_id: $in:[]}
$group: ....
Nalik
Но это уже получается 2 запроса, я не вижу ничего плохого в этом. Но мне казалось, можно как-то Join-ом решить и это было бы удобно)
Про это знаю
const selectedChatIds = await chats_user.distinct("chat_id", { user_id: 1, leave_time: 0 });
await chats_user.aggregate([
{ $match: { chat_id: { $in: selectedChatIds } } },
{ $group: { _id: "$chat_id", count: { $sum: 1 } } },
]);
Nalik
Вообще не очень понятно, почему lookup count выполняется так долго
{
$lookup: {
from: 'chats_user',
let: { chatID: '$chat_id' },
pipeline: [
{ $match: { $expr: { $and: [{ $eq: ['$chat_id', '$$chatID'] }, { $eq: ['$leave_time', 0] }] } } },
{ $count: 'userCount' },
],
as: 'dbUserCount',
},
}
Nick
Вообще не очень понятно, почему lookup count выполняется так долго
{
$lookup: {
from: 'chats_user',
let: { chatID: '$chat_id' },
pipeline: [
{ $match: { $expr: { $and: [{ $eq: ['$chat_id', '$$chatID'] }, { $eq: ['$leave_time', 0] }] } } },
{ $count: 'userCount' },
],
as: 'dbUserCount',
},
}
эксплейн что говорит?
Nick
Я если честно не знаю, что это
https://www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/#return-information-on-aggregation-pipeline-operation
Nalik
Nick
чтоб посмотреть что делает база при выполнении запроса и куда уходит время надо еще передать параметр executionStats в экплейн
Nick
Но это уже получается 2 запроса, я не вижу ничего плохого в этом. Но мне казалось, можно как-то Join-ом решить и это было бы удобно)
Про это знаю
const selectedChatIds = await chats_user.distinct("chat_id", { user_id: 1, leave_time: 0 });
await chats_user.aggregate([
{ $match: { chat_id: { $in: selectedChatIds } } },
{ $group: { _id: "$chat_id", count: { $sum: 1 } } },
]);
вообще тут имелось в виду, что вы не делаете доп запрос, а у вас уже есть инфа о юзере и его чатах, например вытащенная из его профиля
Nalik
{
explainVersion: '2',
stages: [
{
'$cursor': [Object],
nReturned: 30,
executionTimeMillisEstimate: 0
},
{
'$lookup': [Object],
totalDocsExamined: 300030,
totalKeysExamined: 300030,
collectionScans: 0,
indexesUsed: [Array],
nReturned: 30,
executionTimeMillisEstimate: 542
},
Nalik
Ну что оно выполняется до*** 542 я и так знал
Nalik
и что затронуто 300к документов я тоже знал. Поскольку создал 10к пользователей. Создал 30 чатов и в каждый из них добавил эти 10к пользователей. Соответственно таблица с информацией о чатах пользователя содержит 300к записей
Nalik
Такой стресс-тест )
Nick
Nalik
Много :)
Если не делать такого подсчета, то операция выполняется 20 мс.
Nalik
Если предположить, что одним запросом можно сделать выборку для 200 чатов время сразу кратно увеличиться.
Если предположить, что пользователей будет больше, время кратно увеличиться.
В общем нужно сразу проектировать так, чтобы таких проблем не было
Nalik
Я прихожу к мысли, что для этого не нужно ничего считать. По крайней мере более чем для 1 чата одновременно.
Nick
{
explainVersion: '2',
stages: [
{
'$cursor': [Object],
nReturned: 30,
executionTimeMillisEstimate: 0
},
{
'$lookup': [Object],
totalDocsExamined: 300030,
totalKeysExamined: 300030,
collectionScans: 0,
indexesUsed: [Array],
nReturned: 30,
executionTimeMillisEstimate: 542
},
раскройте полностью дерево и на https://pastila.nl/
Nalik
Может и не такая плохая идея делать updateMany при приходе нового сообщения. Делать асинхронно, чтобы не тормозить возврат.
Все таки действительно не предполагается наличие такого большого количества пользователей..
yopp
относительно быстро считать можно используя covered query, но они не будут работать внутри $lookup из-за $expr
Nalik
Но это что касается количества непрочитанных сообщений.
Для подсчета всех пользователей в чате можно так же создавать отдельное поле user_count где при вступлении в чат увеличивать счетчик. При выходе уменьшать..
Nalik
А если записей будет не 300к, а 300 миллионов.
yopp
если вы можете сделать счётчики, это конечно будет быстрее
yopp
и вы их никогда не угадаете
Nalik
Да я все понимаю)
Nalik
Но зная о потенциальных слабых местах я не могу спокойно спать)
Nalik
О масштабировании это не сейчас)
yopp
если у вас нет того объёма данных о котором вы беспокоитесь, то у вас нет слабых мест 🙂
очень много проектов которые я вижу едут до огромных MRR не всегда имея даже индексы по очень горячим местам
yopp
и проблемы у них в совершенно не тех местах, которые тут обычно обсуждают как узкие 🙂
Nalik
Nalik
Хотя я понимаю, что скорее всего эти решения были сделаны за зарплату не с большой мотивацией)
Алексей
а может они были сделаны с мыслью что у вк апи реализовано так и значит очевидно что надо так? 😉
yopp
когда работаешь консультантом видишь что самый эффективный подход это «сносно делай, сносно будет»
попытка оптимизировать проблему которой у вас нет, это попытка предсказать будущее. а человечество в предсказании будущего на сегодняшний день удивительно плохо
Nalik
Но в данном случае мое видение сходится с реализацией ВК АПи
yopp
по этому есть смысл тратить ресурсы там, где они мешают доставлять ценность конечному потребителю
Nalik
yopp
и уже когда у вас будет понимание что текущее решение ну совсем уже никуда не едет и поддерживать его архисложно — делать новое решение с новой архитектурой
Nick
Nalik
Ну так там и видно:
totalDocsExamined: 300030,
totalKeysExamined: 300030,
Nalik
Подскажите мне по поводу количества непрочитанных сообщений в чате, как бы сделали это вы?
наиболее удачным, хоть и компромиссным мне кажутся 2 решения:
1. Для каждого сообщения хранить локальный числовой ID относительно чата которому он принадлежит. И в итоге все сводится к простой арифметике: от последнего отнять непрочитанное (подробнее тут: https://t.me/mongodb_ru/132277 )
2. асинхронный updateMany по полю unread_count при приходе нового сообщения :)
Nalik
Первый способ мне кажется надежнее. Но в разы муторнее.