مشکل N+1 (N+1 problem)
در مقاله ی دیگری به طور مفصل در مورد مشکل N+1 صحبت شده است و با یک مثال، آن را برای شما یادآوری می کنیم. این مشکل زمانی اتفاق می افتد که N عبارت جستجوی (Query) اضافی در کد داشته باشیم. به چه منظور؟ برای واکشی داده هایی که می توانستیم هنگام واکشی اولیه (initial fetch)، به آن ها دست پیدا کنیم.
اگر جمله زیر را درک کرده باشید، می توانید به بخش های بعد، بروید اما اگر هنوز موضوع برای شما مبهم است، پیشنهاد می کنم به مطالعه همین بخش ادامه دهید. ما به شما کمک می کنیم تا به راحتی این مشکل را درک کنید.
تشبیه دستور
ابتدا بیاید همه ی آن کوئری ها، SQL و برنامه های تحت وب را کنار بگذارید و تصور کنید که دوست دارید کیک بپزید.
برای این مثال، بیایید تصور کنیم که یخچال و انبار مواد غذایی شما در آشپزخانه ی منزل نیست، بلکه در اتاق زیر شیروانی قرار دارد، و شما را مجبور می کند هر وقت چیزی از داخل یخچال نیاز داشتید، از آشپزخانه بیرون بیایید و از پله ها بروید.
حال، بیایید تصور کنیم که به اتاق خوابتان رفته اید تا کتاب آشپزی را بردارید، دستور کیک شکلاتی را پیدا کرده و با آن به آشپزخانه برگردید.
خط اول را بررسی می کنید:
200 گرم شکلات تیره.
شما برای آوردن یک قرص شکلات به اتاق زیر شیروانی می روید، سپس به آشپزخانه بر می گردید و سطر دوم را می خوانید:
3 عدد تخم مرغ
یک بار دیگر، از پله ها بالا می روید تا آن 3 تخم مرغ را بیاورید، به آشپزخانه برگردید و خطوط بعدی دستور را بخوانید:
100 گرم کره
کمی خسته از این طرف و آن طرف رفتن، دوباره به سمت اتاق زیر شیروانی می روید. با خودتان می گویید اگر فقط راه ساده تری وجود داشت ...
اگر به این روش کیک را بپزید، دقیقا همان چیزی را تجربه می کنید که هنگام بروز مشکل N+1 در کوئری، رخ می دهد. ORM (Object Relational Mapper) شما پس از اولین کوئری (در هنگام تهیه ی موارد برای دستور کار)، مجبور به انجام N پرسش اضافی (سفرهای رفت و برگشت برای واکشی مواد اولیه) می شود.
این مشکل چه زمانی اتفاق می افتد؟
این مشکل زمانی اتفاق می افتد که نیاز داریم فرزندان را از رابطه ی parent-child (رابطه والدین و فرزندان) بارگیری کنیم. (بیشتر در روابط یک به چند). در بیشتر ORM ها، lazy-loading به طور پیش فرض فعال است، بنابراین درخواست برای رکورد والد داده می شود و سپس برای هر رکورد فرزند یک کوئری درخواست می شود. همان طور که انتظار می رود، انجام کوئری های N+1 به جای یک کوئری، پایگاه داده ی ما را با کوئری پر می کند. این همان چیزی است که ما باید از آن پرهیز کنیم.
می خواهیم با یک مثال این موضوع را شرح دهیم. یک وبلاگ ساده را در نظر بگیرید که دارای مقاله های زیادی ست و نویسندگان مختلف آن ها را منتشر کرده اند. شبه کد زیر، روابط بین مدل ها را نشان می دهد:
#Articles model
class Article
belongs_to :author
#Authors model
class Author
has_many :posts
ما می خواهیم 5 مقاله ی آخر را، همراه با عنوان و نام نویسنده ی آن ها لیست کنیم. شبه کد آن به صورت زیر خواهد بود:
#In our controller
$recent_articles = Article.order(desc).limit(5)
#in our view file
$recent_articles.each do |article|
Title: <%= article.title %>
Author:<%= article.author.name %>
شبه کد بالا، 6 درخواست (5 + 1) را به پایگاه داده ارسال می کند، 1 مورد برای 5 مقاله آخر و سپس 5 مورد برای نویسندگان مربوط به آن مقاله ها. در این حالت، از آن جا که تعداد درخواست ها را به 5 مورد محدود می کنیم، این مسئله تاثیر زیادی بر عملکرد برنامه ی ما ندارد. اما برای کوئری های بیشتر، این می تواند کشنده باشد.
بررسی مشکل در لاراول
زمانی که می خواهیم یک برنامه ی تحت وب را با یک دیتابیس ارتباط برقرار می کند، ایجاد کنیم باید دو نکته را در نظر بگیریم:
- کوئری های دیتابیس را به حداقل برسانیم.
- مصرف حافظه (Ram) را به حداقل برسانیم.
رعایت این نکته ها، تاثیر چشم گیری بر عملکرد برنامه ی ما خواهد داشت. توسعه دهندگان معمولا نکته ی اول را به خوبی رعایت می کنند. از تکنیک هایی مانند eager loading برای محدود کردن نمایش داده های دیتابیس استفاده می کنند. با این حال در مورد نکته ی دوم که به حداقل رساندن مصرف حافظه است، نمی توانند عملکرد خوبی داشته باشند. در حقیقت، کوئری های دیتابیس را به حداقل می رسانند ولی مصرف حافظه را بالا می برند و با این کار، آسیب بیشتری وارد می کنند.
در این مقاله مروری بر مشکل N+1 داشتیم و در انتها به توضیح مختصری در مورد آن در لاراول پرداختیم. در مقاله های آینده، شیوه ی برطرف کردن این مشکل در لاراول را بررسی خواهیم کرد. پس با ما همراه باشید. ما منتظر نظر های سازنده شما هستیم.
منابع
https://medium.com/doctolib/understanding-and-fixing-n-1-query-30623109fe89
https://medium.com/swlh/solving-n-1-query-without-creating-memory-issue-in-laravel-d02d77c5fccc
https://reinink.ca/articles/dynamic-relationships-in-laravel-using-subqueries
https://pociot.dev/1-finding-n1-queries-in-laravel#eager-loading
https://beyondco.de/docs/laravel-query-detector/installation
https://beyondco.de/docs/laravel-query-detector/installation
https://www.codecheef.org/article/laravel-and-n-1-problem-how-to-fix-n1-problem