راهنمای جامع بهینهسازی عملکرد در EF Core: سرعت را قربانی نکنید
۱. تشخیص گلوگاهها: نمیتوانید آنچه را نمیبینید اصلاح کنید
قبل از هرگونه بهینهسازی، باید بدانید مشکل کجاست. حدس زدن در پرفورمنس خطرناک است.
ابزارهای مانیتورینگ
-
LogTo در Console: سادهترین راه برای دیدن کوئریهای SQL تولید شده در محیط توسعه.
-
SQL Server Profiler / Azure Data Studio: برای مشاهده دقیق آنچه به دیتابیس ارسال میشود.
-
MiniProfiler: ابزاری عالی برای دیدن مدت زمان اجرای هر کوئری در صفحات وب.
-
Application Insights: برای محیطهای Production حیاتی است.
نکته مهم: همیشه به تعداد کوئریهای ارسالی و زمان اجرای آنها (Execution Time) دقت کنید.
۲. مشکل کلاسیک N+1 (و راه حل آن)
یکی از رایجترین دلایل کندی در EF Core، مشکل N+1 است. این اتفاق زمانی میافتد که شما یک لیست از موجودیتها را واکشی میکنید (1 کوئری) و سپس برای دسترسی به موجودیتهای وابسته هر کدام، یک کوئری جداگانه اجرا میشود (N کوئری).
سناریوی مشکلدار (Lazy Loading)
فرض کنید لیستی از "سفارشها" (Orders) دارید و میخواهید نام "مشتری" (Customer) هر سفارش را چاپ کنید:
// این کد ابتدا تمام سفارشها را میگیرد (1 کوئری)
var orders = context.Orders.ToList();
foreach (var order in orders)
{
// اینجا برای هر سفارش، یک کوئری به دیتابیس میزند تا مشتری را پیدا کند (+N کوئری)
Console.WriteLine(order.Customer.Name);
}
راه حل: Eager Loading
استفاده از Include باعث میشود EF Core با استفاده از JOIN، دادههای وابسته را در همان کوئری اول واکشی کند.
var orders = context.Orders
.Include(o => o.Customer) // دادههای مشتری را همین حالا بیار
.ToList();
۳. جادوی AsNoTracking برای کوئریهای فقط خواندنی
EF Core به صورت پیشفرض تمام موجودیتهایی که واکشی میکند را در حافظه Track (ردیابی) میکند تا تغییرات آنها را تشخیص دهد و در SaveChanges اعمال کند. این فرآیند هزینه حافظه و CPU دارد.
اگر هدف شما فقط نمایش دادههاست و قرار نیست آنها را ویرایش کنید، حتماً از AsNoTracking استفاده کنید.
مقایسه عملکرد
-
با Tracking: ایجاد Snapshot از دادهها، بررسی مداوم تغییرات.
-
بدون Tracking: صرفاً نگاشت دادهها از SQL به اشیاء C#.
// بهینهترین حالت برای لیستهای نمایش (Grids, Reports)
var products = context.Products
.AsNoTracking()
.Where(p => p.Price > 1000)
.ToList();
۴. فقط آنچه نیاز دارید را بردارید (Projection)
آیا واقعاً به تمام ۵۰ ستون جدول Users نیاز دارید وقتی فقط میخواهید نام کاربر را در هدر سایت نمایش دهید؟ واکشی ستونهای اضافی باعث افزایش ترافیک شبکه و مصرف حافظه میشود.
استفاده از Select برای انتخاب فیلدهای خاص (Projection) یکی از موثرترین روشهای بهینهسازی است.
// بد: واکشی تمام ستونها (شامل عکس پروفایل با حجم بالا و...)
var users = context.Users.ToList();
// عالی: فقط ستونهای مورد نیاز
var userNames = context.Users
.Select(u => new { u.Id, u.Name }) // Anonymous Type
.ToList();
۵. فیلترینگ سمت کلاینت در برابر سمت سرور
یکی از تغییرات مهم در EF Core 3.0 به بعد، سختگیری در مورد Client Evaluation بود. شما باید مطمئن شوید که شرطهای Where شما به SQL ترجمه میشوند، نه اینکه کل دادهها به حافظه رم سرور بیاید و آنجا فیلتر شود.
اشتباه رایج
استفاده از متدهای C# که معادل SQL ندارند در داخل کوئری.
// این کد ممکن است باعث شود تمام رکوردها فیلتر نشده به حافظه بیایند (اگر EF نتواند ترجمه کند)
var result = context.Employees
.Where(e => SomeCustomCSharpMethod(e.Name))
.ToList();
راه حل: تا حد امکان از توابع استاندارد LINQ که قابل ترجمه به SQL هستند استفاده کنید یا دادهها را ابتدا فیلتر اولیه (در دیتابیس) و سپس فیلتر نهایی (در حافظه) کنید (فقط اگر حجم داده کم شده باشد).
۶. انفجار کارتزین و Split Queries
وقتی شما چندین رابطه یک-به-چند (Collection) را Include میکنید، دیتابیس برای بازگرداندن نتیجه مجبور به ایجاد ضرب دکارتی (Cartesian Product) میشود که حجم دادههای تکراری را به شدت افزایش میدهد.
راه حل: AsSplitQuery (معرفی شده در EF Core 5)
این متد به EF میگوید به جای یک کوئری غولپیکر با چندین JOIN، کوئری را به چند کوئری کوچکتر و بهینه تقسیم کند.
var authors = context.Authors
.Include(a => a.Books)
.Include(a => a.Awards)
.AsSplitQuery() // این دستور کوئریها را تفکیک میکند
.ToList();
توجه: در AsSplitQuery باید مراقب سازگاری دادهها (Data Consistency) باشید، زیرا بین اجرای کوئری اول و دوم ممکن است دادهها تغییر کنند (هرچند در اکثر سناریوهای خواندن مشکلی نیست).
۷. عملیاتهای گروهی (Bulk Operations)
تا قبل از EF Core 7، برای آپدیت کردن ۱۰۰۰ رکورد، باید ۱۰۰۰ رکورد را واکشی میکردید، تغییر میدادید و سپس SaveChanges را صدا میزدید.
در نسخههای جدید (EF Core 7 و 8)، متدهای ExecuteUpdate و ExecuteDelete معرفی شدهاند که مستقیماً روی دیتابیس عمل میکنند بدون اینکه دادهها را به حافظه بیاورند.
// روش جدید و فوقسریع: بدون واکشی داده به حافظه
context.Products
.Where(p => p.Category == "Old")
.ExecuteUpdate(s => s.SetProperty(p => p.Price, p => p.Price * 1.1));
۸. نکات تکمیلی و پیشرفته
-
ایندکسگذاری (Indexing): هیچکدام از تکنیکهای بالا اگر دیتابیس شما ایندکسهای مناسب (روی کلیدهای خارجی و ستونهای جستجو) نداشته باشد، کارساز نخواهد بود. همیشه Execution Plan دیتابیس را چک کنید.
-
استفاده از Compiled Queries: برای کوئریهایی که بسیار پرتکرار هستند (مثلاً هزاران بار در ثانیه)، هزینه تبدیل LINQ به SQL قابل توجه است. EF.CompileQuery میتواند این ترجمه را کش کند.
-
Pagination (صفحهبندی): همیشه از Skip و Take استفاده کنید تا حجم دادههای برگشتی مدیریت شود.
خلاصه تکنیکهای کلیدی
| تکنیک | کاربرد | تاثیر بر عملکرد |
| AsNoTracking | کوئریهای فقط خواندنی | کاهش مصرف حافظه و CPU |
| Select (Projection) | انتخاب ستونهای خاص | کاهش ترافیک شبکه و IO دیتابیس |
| Include | جلوگیری از N+1 | کاهش شدید تعداد رفت و برگشت به دیتابیس |
| AsSplitQuery | روابط یک-به-چند پیچیده | جلوگیری از انفجار حجم دادهها |
| ExecuteUpdate/Delete | آپدیت/حذف گروهی | حذف سربار واکشی و Change Tracking |
نتیجهگیری
بهینهسازی در EF Core به معنای کنار گذاشتن LINQ و نوشتن Stored Procedure برای همه چیز نیست. با درک نحوه ترجمه کوئریها، استفاده صحیح از AsNoTracking، مدیریت بارگذاری دادهها (Eager vs Lazy) و بهرهگیری از ویژگیهای مدرن مثل ExecuteUpdate، میتوانید به سرعتی بسیار نزدیک به Dapper یا ADO.NET دست پیدا کنید، در حالی که همچنان از مزایای توسعه سریع EF Core بهره میبرید.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.