بهینهسازی انقلابی مکانیزم Split Queries در EF Core 11
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۱۸ ۰۹:۲۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
Include مشخص شدهاند، از راهبرد کوئری واحد (Single Query) استفاده میکند. در این راهبرد، تمام دادهها در قالب یک دستور SELECT مجهز به پیوندهای چندگانه (مبتنی بر LEFT JOIN) ترجمه و اجرا میشوند.AsSplitQuery، دستورات را تفکیک کنند؛ به طوری که یک کوئری برای موجودیت ریشه و کوئریهای جداگانهای برای هر یک از مجموعههای فرزند اجرا شود. با این حال، تا نسخه EF Core 10، معماری ساخت کوئری در این مکانیزم با نوعی ناپختگی و عدم بهینهسازی در بخش روابط مرجع (Reference Navigations) مواجه بود. در این مقاله به بررسی تحول شگرف و بهینهسازی هوشمندی که در EF Core 11 برای رفع این مشکل و بهبود چشمگیر کارایی (Performance) اعمال شده است، میپردازیم.Blog) داریم که دارای یک رابطه مرجع یا تکمقداری (مانند نوع وبلاگ یا BlogType) و یک رابطه مجموعهای (مانند لیست مقالات یا Posts) است. قطعه کد زیر را برای واکشی دادهها در نظر بگیرید:var result = context.Blogs
.Include(x => x.BlogType) // ناوبری مرجع (Reference Navigation)
.Include(x => x.Posts) // ناوبری مجموعهای (Collection Navigation)
.AsSplitQuery()
.ToList();SELECT [b].[Id], [b].[BlogDetailId], [b].[BlogTypeId], [b].[Name], [b0].[Id], [b0].[Type] FROM [Blogs] AS [b] LEFT JOIN [BlogType] AS [b0] ON [b].[BlogTypeId] = [b0].[Id] ORDER BY [b].[Id], [b0].[Id]
SELECT [p].[Id], [p].[BlogId], [p].[Title], [b].[Id], [b0].[Id] FROM [Blogs] AS [b] LEFT JOIN [BlogType] AS [b0] ON [b].[BlogTypeId] = [b0].[Id] INNER JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId] ORDER BY [b].[Id], [b0].[Id]
BlogType به طور کاملاً غیرضروری باز هم JOIN شدهاست! نهتنها جدول، پیوند خورده، بلکه فیلد کلید آن انتخاب شده و در بند ORDER BY نیز قرار گرفته است. از منظر منطق پایگاه داده، برای انتساب و متصل کردن هر Post به وبلاگ مربوطه، داشتن کلید اصلی خود وبلاگ (BlogId) کفایت میکند و اطلاعات ناوبری تکمقداری (مرجع) هیچ نقشی در این میان ندارند.⚠️ اثر تجمعی ناکارآمدی (Cumulative Degradation Effect):
اگر در یک پروژه واقعی، موجودیت ریشه شما به جای یک رابطه مرجع، دارای پنج یا ده رابطه مرجع (مانندAuthor،Category،Statusو...) باشد، این نقص فنی به صورت تصاعدی تشدید میشود. در EF Core 10، به ازای تکتک کوئریهای مجموعهای فرزند، تمام آن ۵ یا ۱۰ جدول مرجع مجدداًJOINوORDER BYمیشدند. نتیجه این امر، تحمیل بار پردازشی سنگین به موتور بهینهساز پایگاه داده (Query Planner)، اشغال بیهوده حافظه پنهان و افت شدید کارایی کل سیستم بود.
SELECT [p].[Id], [p].[BlogId], [p].[Title], [b].[Id] FROM [Blogs] AS [b] INNER JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId] ORDER BY [b].[Id]
BlogType، اعم از دستور LEFT JOIN، فیلدهای انتخابی در سگمنت SELECT و ستونهای اضافی در ORDER BY به طور کامل حذف شدهاند. این بهینهسازی ساختاری، پیچیدگی کامپایل و اجرای کوئری را در سمت DBMS به شدت کاهش میدهد.BenchmarkDotNet مورد بررسی قرار گرفته است. در این بنچمارک، یک پایگاه داده درونحافظهای SQLite با حجم داده اولیه ۵,۰۰۰ وبلاگ و ۵ مقاله به ازای هر وبلاگ (در مجموع ۲۵,۰۰۰ مقاله) ایجاد شد. موجودیت وبلاگ در این سناریو دارای ۳ ناوبری مرجع (Author, Category, Series) و ۱ ناوبری مجموعهای (Posts) است تا اثر هرسسازی بستر واقعیتری پیدا کند.[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net10_0)]
[SimpleJob(RuntimeMoniker.Net11_0)]
public class SplitQueryBenchmarks
{
private SqliteConnection _connection = null!;
private DbContextOptions<BloggingContext> _options = null!;
[GlobalSetup]
public void Setup()
{
_connection = new SqliteConnection("Data Source=:memory:");
_connection.Open();
_options = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlite(_connection)
.Options;
using var context = new BloggingContext(_options);
context.Database.EnsureCreated();
Seeder.Seed(context, blogCount: 5_000, postsPerBlog: 5);
}
[GlobalCleanup]
public void Cleanup() => _connection.Dispose();
[Benchmark]
public List<Blog> SplitQuery()
{
using var context = new BloggingContext(_options);
return BlogQuery(context).AsSplitQuery().ToList();
}
private static IQueryable<Blog> BlogQuery(BloggingContext context)
=> context.Blogs
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Category)
.Include(b => b.Series)
.Include(b => b.Posts);
}| Method | Runtime | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated |
| SplitQuery | .NET 10.0 (EF 10) | 68.53 ms | 1.358 ms | 2.742 ms | 67.54 ms | 4375.0 | 1000.0 | 375.0 | 33.35 MB |
| SplitQuery | .NET 11.0 (EF 11) | 62.07 ms | 1.213 ms | 2.788 ms | 61.10 ms | 4111.1 | 1333.3 | 444.4 | 30.15 MB |
💡 نکته مهندسی و تحلیل معماری تجاری:
توجه داشته باشید که این بنچمارک روی دیتابیس درونحافظهای SQLite اجرا شده است که در آن هزینه JOIN و ارتباطات به دلیل عدم وجود لایه شبکه فیزیکی بسیار ناچیز است. در محیطهای عملیاتی صنعتی (Enterprise) که از بانکهای اطلاعاتی مجزا مانند SQL Server یا PostgreSQL استفاده میکنند، حذف لایههای اضافی پیوند (JOIN) و کاهش پهنای باند شبکه، تاثیر به مراتب چشمگیرتری (گاهی تا چندین برابر بهبود کارایی) در کاهش تاخیر (Latency) کل لایه داده خواهد داشت.AsSplitQuery یک راهکار همهمنظوره یا اصطلاحاً گلوله نقرهای (Silver Bullet) نیست. توسعهدهندگان ارشد باید همواره سبکسنگین کردنهای (Trade-offs) زیر را در طراحی معماری لایه داده مد نظر قرار دهند: