9 نکته برای بهینه سازی کوئری های دیتابیس در لاراول

امروز می خواهم چند تا نکته برای افزایش سرعت کوئری گرفتن در لارول رو بررسی کنم.وقتی اپلیکشن شما بزرگ و بزرگ تر می شود حجم اطلاعاتی که در دیتابیس نگهداری می کنید هم به صورت تصاعدی افزایش پیدا می کند. اینجا است که متوجه کندی و افزایش ریسپانس تایم (Response Time) لاراول می شوید. اما با رعایت کردن و اجرای چند نکته بسیار ساده می توانید سرعت ، واکشی دیتای اپلیکیشن تان را افزایش دهید .1.دریافت اطلاعات حجیم از دیتابیسکاری که ما به طور معمول برای دریافت دیتا از دیتابیس انجام می دهیم به صورت زیر است:با استفاده از eloquent$products = Product::all();با استفاده از query builder $products = DB::table('products')->get();در این روش ابتدا کل دیتای جدول از دیتابیس گرفته می شود و سپس در حافظه ذخیره می شود و بعد از پردازش ،نتیجه آن در حافظه نگهداری می شود. اما ایرادی که در این روش وجود دارد این است که اگر تعداد سطر های جدول شما زیاد باشد( مثلا 2 میلیون ) حافظه سرور شما در کسری از ثانیه پر شده و سرور شما دچار اختلال می شود.هنگام دریافت داده های بزرگ در لاراول باید دیتا را به قسمت های کوچک تقسیم کرده پردازش را روی آن بخش کوچک انجام داده و سپس بخش جدیدی واکشی شود(fetch).برای جلوگیری از این اشکال می توان چند راهکار ارایه داد:. استفاده از chunk$products = Product::chunk(100, function($products){ // process product here }); //or $products = DB::table('products')->chunk(100, function ($products){ // process product here });در این روش ما ابتدا 100 سطر از دیتابیس میگیریم پردازش های لازم را روی آن انجام می دهیم و سپس 100 سطر دیگر میگیریم و این کار را تا آخر ادامه میدهیم.توجه داشته باشید در این روش تعداد کوئری ها به دیتابیس زیاد می شود . اما در عوض حافظه سرور مدیریت می شود. تعداد row های دریافتی بستگی به تعداد فیلد های دیتابیس و حجم دیتابیس و البته حافظه (RAM) سرور شما دارد.. استفاده از cursorforeach(Product->cursor() as $product){ //process a single product } //or foreach(DB::table('products')->cursor() as $product){ //process a single product } دقیقا برعکس روش قبل که طی چندین کوئری دیتا را از دیتابیس واکشی می کرد در این روش ما تمامی اطلاعات را در قالب یک کوئری از دیتابیس دریافت می کنیم اما سطر ها را یکی یکی وارد مدل لارول (Eloquent model) می کنیم که باعث بهینه شدن استفاده لارول از حافظه می شود.از این روش زمانی استفاده کنید که دچار کمبود حافظه در سرور لارول شده اید و حافظ ای جداگانه برای دیتابیس خود تخصیص داده اید اگه حافظه دیتابیس شما با حافظه اپلیکیشن یکی باشد ، این روش به کار شما نمی آید و باز دچار کمبود حافظه می شوید.. استفاده از chunkById$products= Product::chunkById(100, function($products){ }); //or $products= DB::table('products')->chunkById(100, function ($products){ });تفاوتی که chunkById با chunk در نحوه تولید SQL خروجی می باشد.chuck:select * from products offset 0 limit 100chunkById:select * from products where id > 100 order by id asc limit 100در روش chunk با استفاده از offset پست ها تقسیم می شوند که روش بسیار کندی است و ما همواره سعی می کنیم تا جایی امکان دارد از offset استفاده نکنیم.اما chunkById با استفاده از where و جستجو در فیلد (id) 100 سطر را نتخاب می کند.دقت داشته باشید که فیلد id باید primary keyو auto incrementباشد.2. فقط ستون هایی که نیاز دارید را select کنیدبه طور معمول زمانی که می خواهیم یک سطر از دیتابیس را واکشی کنیم از این روش استفاده می کنیم $products= Product::find(1); //or $products= DB::table('products')->where('id','=',1)->first();در این روش ما فارغ از این که فقط به یک فیلد (برای مثال نام محصول ) نیاز داریم تمامی ستون های جدول را دریافت می کنیم که باعث کند شدن اجرای کوئری و مصرف بیشتر حافظه ما می شود.استفاده از select $products= Product::select([' id' , 'name' ])->find(1); //or $products= DB::table('products')->where('id','=',1)->select(['id' , 'name' ])->first();اما با استفاده از select می توانیم تنها ستون هایی که نیاز داریم را واکشی کنیم.3. استفاده از pluck برای واکشی تعداد کمی از ستون های جدول این روش به طور کلی ربطی به کوئری گیری از دیتابیس ندارد و تمرکزش بر کاهش زمان پردازش بعد از دریافت دیتا از دیتابیس است.همان طور که در نکته بالا گفتم برای محدود کردن تعداد ستون های دریافتی از select استفاده می کنیم.اما روشی که لاراول برای آن انجام می دهد به صورت زیر است:کوئری select id,name from products where id = 1 limit 1اجرا و اطلاعات از دیتابیس گرفته می شود.برای هر کدام از سطر ها (rows) یک شی از Product modelساخته می شود. سپس تمامی اشیا که ازProduct model ساخته شده بود در یک Collection قرار می گیرد.و در نهایت Collection برگردانده می شد.حال برای دسترسی به idو nameبه صورت زیر عمل می کنیم:foreach ($products as $product){ $product->id; $product->name; }اما ما در این حالت فقط نیاز به گرفتن 2 فیلد از دیتابیس داریم و در عمل به instance ی که از مدل Product برای ما ساخته شده است نیازی نداریم.پس می توانیم با استفاده از pluck به راحتی و با کمترین میزان پردازش به اطلاعت مورد نیاز دسترسی پیدا کنیم.$products = Product::pluck('name', 'id'); //or $products = DB::table('products ')->pluck('name','id');در این روش :کوئری select id,name from products where id = 1 limit 1اجرا و اطلاعات از دیتابیس گرفته می شود.یک آرایه که کلید (key) آن idو مقدار آن (value) nameاست ساخته می شود.و در نهایت آن آرایه برگردانده می شد.و حال به صورت زیر می توان به id , name دسترسی داشت:foreach ($products as $id=> $name){ // $id is the title of a product // $name is the slug of a product }دقت داشته باشید در این روش مدل ساخته نشده و سرعت پردازش و مصرف حافظه بعد از دریافت دیتا از دیتابیس بهینه شده است.4. عدم شمارش تعداد سطر ها با استفاده از collection لاراولبه طور معمول زمانی که می خواهیم تعداد سطر ها را بشماریم از روش زیر استفاده میکنیم:$products= Product::all()->count(); //or $products= DB::table('products')->get()->count();در روش بالا ابتدا با استفاده از کوئری زیر :select * from productsتمامی سطر ها را از دیتابیس واکشی می شود و سپس آن را در collection لاراول لود کرده و تعداد را شمارش می کند .استفاده از این روش برای شمارش هنگامی که حجم جدول پایین است مشکلی ندارد اما زمانی که حجم بالا باشد به سادگی دچار کمبود حافظه می شویم.راهکار استفاده از روش زیر است :$products= Product::count(); //or $products= DB::table('products')->count();لاراول برای اجرای روش بالا ، کوئری زیر را اجرا میکند :select count(*) from productsکه روش استانداردی برای شمارش است.توجه داشته باشید که به طور کلی عمل شمارش سطر ها (count) در SQL نیاز به پردازش زیادی دارد پس تا جای امکان سعی کنید از count استفاده نکنید.5. استفاده از Eager loading مبحث لود کردن n+1 کوئری که بسیار مهم و در صورت رعایت نشدن می تواند تاثیر چشم گیری بر روی سرعت داشته باشد.به علت گسترده بودن این مبحث در این مطلب توضیح داده نمی شود شما می توانید با یک جستجو ساده مقالاتی در این زمینه پیدا کنید.6. اجتناب از استفاد wildcards در کوئریما معمولا زمانی که میخواهیم یه کلمه را در جدول پیدا کنیم از روش زیر استفاده میکنیم:select * from table_name where column like %keyword%این کوئری کل جدول ما را به طور کامل جستجو می کند. اما اگر بدانیم که عبارت کلیدی ما فقط در ابتدای جمله ها یافت می شود می توان از کوئری زیر استفاده کرد که بسیار سریع تر است.select * from table_name where column like keyword%7. اجتناب از استفاد functions های SQL در whereبه طور کلی استفاده از توابع SQL در where موجب اسکن کل جدول و پردازش بیشتری می شود و باید تا جای امکان سعی کنید از آن استفاده نکنید.برای مثال ما معمولا برای دریافت دیتا براساس تاریخ از روش زیر استفاده می کنیم:$products= Product::whereDate('created_at', '>=', now() )->get();که کوئری که لارول در بک گراند اجرا می کند به صورت زیر است: select * from products where date(created_at) >= 'timestamp-here'که همان طور که مشاهده می کنید از از تابع date استفاده شده است در صورتی که می توانستیم از روش زیر استفاده کنیم:$products = Product::where('created_at', '>=', now() )->get(); که کوئری خروجی آن با استفاده از where می باشد.select * from products where created_at >= 'timestamp-here'8.اجتباب از اضافه کردن ستون های بی شمار به یک جدولیکی از اشتباهاتی که برنامه نویسان هنگام طراحی پایگاه داده انجام می دهند اضافه کردن تعداد بسیار زیادی ستون (Column) به یک جدول است که باعث افزایش حجم دیتای آن جدول می شود . که به خصوص در MYQSL این کندی بیشتر هم هست.راهکار تقسیم ستون ها در جدول های دیگر و جوین(JOIN) کردن آن هنگام واکشی اطلاعت است.اما باید به این نکته توجه شود که تا جای امکان ستون هایی که وابستگی کمتری نسبت به یکدیگر دارند از هم جدا شوند که نیاز به جوین کردن جدول ها تا جای امکان کاهش یابد.برای مثال نام و نام خانوادگی دو فیلدی هستند که وابستگی به یکدگیر دارند و هرجا نیاز به استفاده از "نام"باشد "نام خانوادگی" نیز نیاز است پس اگر این دو فیلد از یکدیگر جداشوند شما همواره نیاز به جوین کردن جدول ها را دارید و این روش عملا به کار شما نمی آید. 9. نحوه دریافت آخرین رکورد ها از جدولبه طور معمول ما برای گرفتن آخرین رکورد ها ( از آخر به اول ) از دیتابیس از روش زیر استفاده می کنیم:$products= Product::latest()->get(); // or $products= Product::orderBy('created_at', 'desc')->get();که در این روش لاراول کوئری زیر را تولید و اجرا می کند:select * from products order by created_at descهمان طور که مشاهده می کنید در این کوئری ابتدا محصولات بر اساس فیلد created_atمرتب شده و در نهایت آخرین رکورد انتخاب می شود.اما باید به این نکته دقت داشته باشید که created_at یک داده متنی می باشد و مرتب سازی آن به مراتب از مرتب سازی اعداد کند تر می باشد.اما اگر شما فیلد idکه auto increment باشد داشته باشید می توانید مرتب سازی را بر اساس idانجام دهید که روش بسیار سریع تری می باشد.$products= Product::latest('id')->get(); // or $products= Product::orderBy('id', 'desc')->get();سخن پایانی :به طور کلی بهینه سازی یک مسئله وابسته به ساختار دیتابیس و کد شماست و باید روش ها نسبت به اپلیکشن شما شخصی سازی شود .شما می توانید با نصب ابزار laravel debugbar کوئری های اجرایی ، میزان مصرف حافظه و زمان پاسخ اپلیکشن خود را سنجیده و با توجه به آن بهینه سازی لازم را انجام دهید.