Заглавието звучи малко пристрастно, но сега, когато привлякох вниманието ви, нека обясня моята гледна точка...

Когато някой реши да използва MongoDB като основна база данни за система, ние разчитаме на нейните характеристики, за да преодолеем подхода на релационните бази данни и ограниченията на други NoSQL решения. MongoDB е добре известен със своите възможности за хоризонтално мащабиране, шардинг и балансиране на натоварването, позволяващи обработка на голям обем данни, постигане на заявки с висока производителност и съхранение на документи без структура, което прави нашия модел на данни напълно гъвкав, наред с много други. Но е естествено, както в повечето приложения, моделът на данните да се развива с течение на времето, за да представи променящата се реалност на системата. Следователно много често срещано събитие е да започнем да разработваме заявки за тези данни, за да поддържаме функционалностите, а не обратното, тоест да дефинираме моделите за достъп на заявките и оттам нашия модел на данни.

Когато в тези ситуации „примитивните“ операции на mongo (например обикновена операция findCollection, прилагаща филтри) не са достатъчни, е възможно да възникне необходимост от получаване на данни по по-сложен начин и това е където започва да се появява използването на агрегации в mongo. Агрегациите са много мощен mongo инструмент, който ни позволява да прилагаме много по-задълбочен синтаксис върху данните, като добавяне на полета при поискване, изчисляване на средни стойности, максимуми, минимуми, комбинирани филтри, групиране по полета, обединяване с други колекции. И именно в този последен случай на съединения започваме да страдаме от нещата, за които mongo не е създаден, а именно да извършваме операции върху множество колекции за един резултат. В случай на агрегиране се представя със стъпката $lookup, също често срещана в mongoose или други orms като „попълване“ на полета (обикновено за n.1 асоциации ).

Операцията $lookup извършва ляво външно присъединяване към неразделена колекция в същата база данни, за да филтрира документи от „присъединената“ колекция за обработка. Техническият директор на MongoDB също „подчертава своите мисли“.

Първоначално $lookup или операцията за попълване може да изглеждат невинни, главно в случаите, когато дефинираме „id“ в колекция, която препраща към един елемент от друга колекция. Понякога може да искаме да попълним масиви от данни, евентуално в ограничен списък, в един документ. Но има и много често срещани сценарии, при които моделираме 1.n релации (които n са неограничени) и искаме да направим заявка за резултати, прилагайки филтри, принадлежащи към свойство на „страна n“ на релацията.

В този пример имаме „книги“ и „автори“ и искаме да групираме книгите по автори и да ги подредим по името на техния автор.

// Book schema
{
   bookId: uuid,
   name: string,
   yearOfPublication: date,
   author: uuid // Ref to Author collection
}   
// Author schema
{
   authorId: uuid,
   name: string,
   lastName: string
}

Следователно, в опростен сценарий, ние ще решим тази заявка, като направим заявка, която включва:

  1. Вземете книгите
  2. За всяка книга свържете съответния автор
  3. Сортирайте резултата по author.name
  4. Вземете първата страница с резултати

Тези стъпки, използващи тази асоциация, могат да бъдат тривиални и всъщност няма да причинят големи проблеми с малък обем данни. Но представете си случая, в който имаме милиони регистрирани книги, тогава нашата производителност бързо ще бъде засегната и ще отнеме от порядъка на няколко секунди, за да се определи очакваният резултат. Това е така, защото всеки елемент в колекцията от книги трябва да бъде попълнен от своята двойка автори, преди филтърът да може да бъде приложен, което е естествено в SQL заявка, но не е така в NOSQL. При тази ситуация, използвайки Mongo, ще платим скъпо за това, че не сме моделирали схемата по различен начин.

Като скоба на тази публикация смятам, че си струва да спомена, че аз лично се сблъсках със сценарий, подобен на този, но подобрен от много по-голяма сложност на модела на обекта. Дотогава екипът на проекта реши да ескалира искане за съвет директно до екипа на MongoAtlas, който удобно предлага персонализирани съвети със своите инженери, нещо изключително полезно за илюстриране на информация с хора, които работят с Mongo всеки ден. В тази „работна сесия“ целият екип успя да присъства и да зададе различни въпроси, да представи проблемите от гледна точка на нашата собствена система, като резултатът беше документ с препоръки и анализ, адаптиран към нашата система.

Най-силната препоръка, нещо, което може да намерите многократно предлагано онлайн, е да моделирате системата въз основа на моделите за достъп до заявки, както е споменато в началото на статията. Това потенциално ще изисква наличието на напълно денормализиран модел, тоест с изобилие от излишък, което предполага наличието на цялата информация, която искаме да получим и върху която искаме да приложим филтри и сортиране, в рамките на една и съща колекция. В този смисъл е важно да се установят моделите за достъп до заявки в две посоки, както писане, така и четене.

Работата върху набор от колекции с излишни данни носи предимството от гледна точка на четене от възможността да използвате полетата на една и съща колекция за решаване на пълна заявка. Въпреки това, когато се запазват тези данни, ще е необходимо да се поддържа обща последователност, гарантираща, че една и съща информация е постоянна на различните места, които съществуват. Това потенциално влошава операциите за запис и усложнява логиката на ниво приложение за актуализиране на записи.

В повечето случаи проблемите с производителността обикновено се появяват при операции за четене, докато операциите за запис не представляват големи изисквания на ниво потребителско изживяване. С други думи, потребителят обикновено има достатъчно търпение да изчака, докато информацията се съхранява, но не и когато осъществява достъп, за да прочете данните, емисия, списък и т.н. За тези ситуации те се надяват, че ще бъде възможно най-незабавно.

Въпреки че тази публикация е специално базирана на предотвратяване на злоупотребата с $lookups, този семинар на Mongo оставя други ключови препоръки като използването на индекси като основен фактор за производителност, най-добри практики за операции за търсене на текст, използване на транзакции, обработка на страници. За тези функционалности ще направя допълнителни публикации за това в бъдещи публикации.

Продължавайки с нашия пример за книги и прилагайки тази концепция за излишък, това, което mongo предлага, е да се промени схемата на книгата, за да се съхранява в нея минималната информация за автора, необходима за разрешаване на заявката. Така че ще получим нещо като:

{
   bookId: uuid,
   name: string,
   author: { 
     authorId: uuid,
     name: string
   }
}

По този начин можем да разрешим оригиналната заявка само като разгледаме колекцията от книги и приложим филтри към author.name, без да е необходимо да правим $lookup или да сканираме общия брой документи.

Но не всичко е щастие, за съжаление... това излишък на информация предполага, че когато актуализираме запис на автор, трябва да го направим и за всяка свързана книга (освен ако не искаме да запазим присвоените оригинални данни). По този начин това, което преди беше проста актуализация, сега се превръща в актуализация на потенциално десетки книги, които използват тези данни. И колкото и странно да звучи, това всъщност е правилният начин да го направим, просто избираме друго място, където да платим за ниска производителност.

Най-удобният за потребителя начин да се справите с това ще бъде да извършите тази масова актуализация във фонов режим, докато на ниво интерфейс показваме съобщения като „промяната ви ще бъде приложена скоро“. Също така е важно да се отбележи, че промяната на името на автора е необичайна ситуация, така че не трябва да се притесняваме да прилагаме тази актуализация постоянно.

Можем също така да моделираме реалност, в която операциите за запис също трябва да са мигновени, в който случай балансът между производителност и излишък ще бъде принуден към по-равномерен баланс, дори изисквайки допълнителна помощ, като управление на данни в кеш памети или друга обработка в фона, който позволява поддържането на здравословна скорост на системата дори при операции за запис. Също така ще разгледам в бъдещи публикации тези видове по-рядко срещани сценарии, при които писането е чувствително към производителността и трябва да постигнем балансиран подход.

Много е вероятно да се случи тези видове промени в схемата да се случат със система, която вече е в пълна разработка и вероятно с производствена версия. Моята препоръка в това отношение е да подготвите стабилни скриптове, които мигрират данни от едно място на друго и, в най-добрия случай, операции, изпълнявани във фонов режим, за да се гарантира тяхната последователност в случай, че нещо не е на мястото си или е неуспешно в процеса на актуализация. За щастие, mongo предлага гъвкавостта за присвояване и премахване на атрибути от схема.

В заключение, $lookup по същество прилага съединение срещу друга таблица, нещо, за което mongo не обещава разумна производителноств голям мащаб на данни. Следователно е важно да го избегнем чрез дефинирането на моделите за достъп до заявки, от които се нуждае нашето приложение, и едва след това да дефинираме какви данни ще съдържа нашата колекция по такъв начин, че да не зависи от другите, когато връща резултати. Това по същество денормализира нашата схема за данни, за да подобри производителността на четене и да влоши производителността на запис. Избягването на $lookups е голяма стъпка към високо ниво на производителност в нашата система, потребителите и заинтересованите страни ще го оценят.

Благодаря за четенето! 📖

Можете да ме последвате в Twitter за повече публикации и страхотни неща.

наздраве! 🤘