# Entity Framework Core: การเพิ่มประสิทธิภาพและแนวทางปฏิบัติที่ดีที่สุดในปี 2026 > คู่มือฉบับสมบูรณ์สำหรับการเพิ่มประสิทธิภาพ Entity Framework Core 10 บน .NET 10 เรียนรู้ AsNoTracking, compiled queries, batch updates, split queries และตัวดำเนินการ LeftJoin - Published: 2026-04-15 - Updated: 2026-04-15 - Author: SharpSkill - Tags: dotnet, entity-framework, performance, best-practices - Reading time: 10 min --- Entity Framework Core 10 ซึ่งเปิดตัวพร้อมกับ .NET 10 LTS ในเดือนพฤศจิกายน 2025 นำเสนอตัวดำเนินการ LeftJoin การค้นหาเวกเตอร์ named query filters และการปรับปรุงการแปล SQL อย่างมีนัยสำคัญ บทความนี้ครอบคลุมแนวทางปฏิบัติที่ดีที่สุดของ EF Core ที่ส่งผลโดยตรงต่อความเร็วในการ query การจัดสรรหน่วยความจำ และความสามารถในการปรับขนาดในสภาพแวดล้อม production > **EF Core 10 ทำงานบน .NET 10 เท่านั้น** > > EF Core 10 เป็นเวอร์ชัน Long-Term Support ที่ได้รับการสนับสนุนจนถึงเดือนพฤศจิกายน 2028 ต้องใช้ SDK และ runtime ของ .NET 10 แอปพลิเคชันที่ยังคงใช้ .NET 8 ควรเลือกใช้ EF Core 8 (LTS) จนกว่าจะดำเนินการย้ายระบบเสร็จสิ้น ## Query Tracking: เมื่อใดควรปิดการใช้งานและเพราะเหตุใด ทุกการเรียกใช้ `DbSet` จะแนบ entity ที่ส่งกลับเข้ากับ change tracker โดยค่าเริ่มต้น Tracker นี้จะเก็บ snapshot ของค่า property เดิม คำนวณความแตกต่างเมื่อเรียก `SaveChanges` และแก้ไขข้อขัดแย้งของ identity ข้าม navigation ต่างๆ ค่าใช้จ่ายนี้ไม่จำเป็นเมื่อข้อมูลถูกส่งตรงไปยัง API response หรือ view model แบบอ่านอย่างเดียว ```csharp // ProductRepository.cs public async Task> GetActiveByCategoryAsync( int categoryId, CancellationToken ct) { return await _context.Products .AsNoTracking() // ข้าม change tracker ทั้งหมด .Where(p => p.CategoryId == categoryId && p.IsActive) .OrderBy(p => p.Name) .Select(p => new ProductDto // ฉายข้อมูลเป็น DTO ที่ระดับฐานข้อมูล { Id = p.Id, Name = p.Name, Price = p.Price }) .ToListAsync(ct); } ``` `AsNoTracking` ขจัดค่าใช้จ่ายในการสร้าง snapshot และการแก้ไข identity ของแต่ละ entity เมื่อใช้ร่วมกับ `Select` จะมีเพียงคอลัมน์ที่จำเป็นเท่านั้นที่ถูกส่งผ่านเครือข่าย สำหรับตารางที่มี 50,000 แถว รูปแบบนี้สามารถลดการจัดสรรหน่วยความจำได้ 40-60% เมื่อเทียบกับการ query full-entity แบบมี tracking สำหรับ context ที่ไม่เคยแก้ไขข้อมูล ให้ตั้งค่าเริ่มต้นตอนลงทะเบียน: ```csharp // Program.cs builder.Services.AddDbContext(options => options.UseSqlServer(connectionString) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); ``` ## Split Queries เพื่อหลีกเลี่ยงการระเบิดแบบ Cartesian การโหลด entity ที่มี collection navigation หลายตัวผ่าน `Include` จะสร้างคำสั่ง SQL เดียวที่มี JOIN เมื่อมีการโหลดสองคอลเลกชันขึ้นไปพร้อมกัน ผลลัพธ์จะขยายตัวเป็นผลคูณ Cartesian ของคอลเลกชันต่างๆ ทำให้ข้อมูลแถวหลักถูกทำซ้ำในทุกการรวมกัน ```csharp // OrderRepository.cs public async Task GetWithDetailsAsync(int orderId, CancellationToken ct) { return await _context.Orders .AsSplitQuery() // SQL หนึ่งคำสั่งต่อหนึ่ง Include .Include(o => o.Items) .ThenInclude(i => i.Product) .Include(o => o.Payments) .Include(o => o.ShippingEvents) .FirstOrDefaultAsync(o => o.Id == orderId, ct); } ``` `AsSplitQuery` แบ่งการโหลดเป็นคำสั่ง SQL แยกกันสำหรับแต่ละ navigation ข้อแลกเปลี่ยนคือ: หลาย round-trip แทนที่จะเป็นหนึ่ง แต่แต่ละชุดผลลัพธ์จะมีขนาดเล็กและหลีกเลี่ยงปัญหาการทำซ้ำแถว EF Core 10 ยังแก้ไขความไม่สอดคล้องของการเรียงลำดับที่มีมานานใน split queries ทำให้การเรียงลำดับ subquery ตรงกับ query หลัก > **เมื่อใดควรใช้ single query** > > Single query ยังคงเหมาะสมกว่าเมื่อโหลด collection navigation เพียงหนึ่งตัว หรือเมื่อ latency ของ round-trip สูง (การเรียกฐานข้อมูลข้ามภูมิภาค) ควรทำ benchmark ทั้งสองโหมดสำหรับรูปแบบการเข้าถึงเฉพาะก่อนตัดสินใจ ## การดำเนินการแบบ Batch ด้วย ExecuteUpdate และ ExecuteDelete เวิร์กโฟลว์ EF แบบดั้งเดิมจะโหลด entity แก้ไข property แล้วเรียก `SaveChanges` สำหรับการดำเนินการจำนวนมากที่ส่งผลกระทบต่อหลายพันแถว วิธีนี้สร้าง instance ที่ถูกติดตามหลายพันตัวและคำสั่ง UPDATE แยกกัน EF Core 7 เปิดตัว `ExecuteUpdateAsync` และ `ExecuteDeleteAsync` เพื่อส่งการดำเนินการเป็นคำสั่ง SQL เดียว EF Core 10 ทำให้ API ง่ายขึ้นอีกโดยรับ lambda ธรรมดาแทน expression tree ```csharp // PromotionService.cs public async Task ApplySeasonalDiscountAsync( int categoryId, decimal discountPercent, CancellationToken ct) { var affected = await _context.Products .Where(p => p.CategoryId == categoryId && p.IsActive) .ExecuteUpdateAsync(s => { s.SetProperty(p => p.Price, p => p.Price * (1 - discountPercent / 100)); s.SetProperty(p => p.LastModified, DateTime.UtcNow); }, ct); // affected = จำนวนแถวที่ถูกอัปเดต } ``` คำสั่งนี้ถูกแปลเป็น `UPDATE ... SET ... WHERE` เดียว ไม่มี entity ใดถูกโหลดเข้าหน่วยความจำ สำหรับการอัปเดต 10,000 แถว เวลาในการดำเนินการลดลงจากหลายวินาที (แบบ tracked) เหลือเพียงมิลลิวินาที รูปแบบเดียวกันใช้ได้กับการลบ: ```csharp // CleanupService.cs public async Task PurgeExpiredSessionsAsync(CancellationToken ct) { await _context.Sessions .Where(s => s.ExpiresAt < DateTime.UtcNow) .ExecuteDeleteAsync(ct); } ``` ## ตัวดำเนินการ LeftJoin ใน EF Core 10 ก่อน EF Core 10 การทำ LEFT JOIN ต้องใช้การรวม `GroupJoin`, `SelectMany` และ `DefaultIfEmpty` ในรูปแบบเฉพาะที่นักพัฒนาส่วนใหญ่ต้องค้นหาทุกครั้งที่ใช้งาน EF Core 10 เพิ่ม `LeftJoin` เป็นตัวดำเนินการ LINQ ระดับหนึ่ง ```csharp // ReportService.cs public async Task> GetEmployeeDepartmentReportAsync( CancellationToken ct) { return await _context.Employees .LeftJoin( _context.Departments, employee => employee.DepartmentId, department => department.Id, (employee, department) => new EmployeeReportDto { FullName = employee.FirstName + " " + employee.LastName, Department = department.Name ?? "Unassigned", HiredAt = employee.HiredAt }) .OrderBy(r => r.FullName) .ToListAsync(ct); } ``` SQL ที่สร้างขึ้นใช้ clause `LEFT JOIN` มาตรฐาน ตัวดำเนินการ `RightJoin` ก็มีให้ใช้งานเช่นกัน ตัวดำเนินการทั้งสองขจัดห่วงโซ่สามเมธอดที่ยาวเหยียดซึ่งจำเป็นต้องใช้ก่อนหน้านี้ ## Named Query Filters สำหรับการกรองหลายวัตถุประสงค์ Global query filters ถูกจำกัดให้มีเพียงหนึ่ง filter ต่อหนึ่งประเภท entity ตั้งแต่ EF Core 2.0 แอปพลิเคชันที่ใช้ทั้ง soft-delete และ multi-tenancy ต้องรวมเงื่อนไขเป็นนิพจน์เดียวและไม่สามารถปิดการใช้งานแบบเลือกได้ EF Core 10 เปิดตัว named query filters ```csharp // AppDbContext.cs protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasQueryFilter("SoftDelete", i => !i.IsDeleted) .HasQueryFilter("Tenant", i => i.TenantId == _tenantId); } ``` Endpoint สำหรับผู้ดูแลระบบสามารถปิด filter soft-delete ในขณะที่ยังคงรักษาการแยก tenant ไว้: ```csharp // InvoiceRepository.cs public async Task> GetAllIncludingDeletedAsync(CancellationToken ct) { return await _context.Invoices .IgnoreQueryFilters(["SoftDelete"]) // filter tenant ยังคงทำงาน .ToListAsync(ct); } ``` สิ่งนี้ขจัดความจำเป็นในการสร้าง extension method ของ `IQueryable` แบบกำหนดเองที่เพิ่มเงื่อนไข filter ด้วยตนเอง ## ความยืดหยุ่นของการเชื่อมต่อและการกำหนดค่า Pooling ข้อผิดพลาดฐานข้อมูลชั่วคราว (เครือข่ายขัดข้อง, Azure SQL failover, connection pool หมด) ทำให้เกิด exception ที่มักทำให้ pipeline ของ request ล้มเหลว EF Core มี logic retry ในตัว แต่ค่าเริ่มต้นต้องมีการกำหนดค่าอย่างชัดเจน ```csharp // Program.cs builder.Services.AddDbContext(options => options.UseSqlServer(connectionString, sqlOptions => { sqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(10), errorNumbersToAdd: null); // retry สำหรับข้อผิดพลาดชั่วคราวทั้งหมด sqlOptions.CommandTimeout(30); // timeout คำสั่ง 30 วินาที })); ``` พารามิเตอร์ pooling ใน connection string ก็มีความสำคัญเช่นกัน: ``` Server=db.example.com;Database=AppDb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=15; ``` `Min Pool Size` เก็บการเชื่อมต่อที่พร้อมใช้งานสำหรับทราฟฟิกที่พุ่งสูง `Max Pool Size` จำกัดจำนวนการเชื่อมต่อที่เปิดทั้งหมดเพื่อป้องกันฐานข้อมูลทำงานหนักเกินไป ค่าเริ่มต้น 100 เหมาะสำหรับเว็บแอปพลิเคชันส่วนใหญ่ แต่บริการที่มี throughput สูงอาจต้องปรับแต่งตามปริมาณ query พร้อมกันจริง > **Logic retry และ transaction** > > Retry อัตโนมัติไม่ทำงานภายใน transaction ที่ผู้ใช้เริ่มต้น ให้ห่อ transaction ทั้งหมดในกลยุทธ์ retry แบบ manual โดยใช้ `context.Database.CreateExecutionStrategy().ExecuteAsync(...)` เพื่อจัดการข้อผิดพลาดชั่วคราวในหลายการดำเนินการ ## กลยุทธ์การทำ Indexing และการวิเคราะห์ Query Migration ของ EF Core สามารถกำหนด index แบบประกาศได้ แต่การเลือกคอลัมน์ที่จะทำ index ต้องอาศัยความเข้าใจรูปแบบ query ```csharp // AppDbContext.cs - OnModelCreating modelBuilder.Entity(entity => { // index แบบผสมสำหรับรูปแบบ query ที่ใช้บ่อย entity.HasIndex(o => new { o.CustomerId, o.Status, o.CreatedAt }) .HasDatabaseName("IX_Order_Customer_Status_Date"); // index แบบมีตัวกรองสำหรับคำสั่งซื้อที่ยังดำเนินอยู่เท่านั้น entity.HasIndex(o => o.Status) .HasFilter("[Status] <> 'Cancelled'") .HasDatabaseName("IX_Order_ActiveStatus"); }); ``` Index แบบมีตัวกรองช่วยลดขนาด index โดยไม่รวมแถวที่ query ไม่เคยเข้าถึง สำหรับตารางที่มีคำสั่งซื้อที่ถูกยกเลิก 80% index แบบมีตัวกรองบนสถานะที่ยังดำเนินอยู่สามารถเล็กกว่าถึง 5 เท่าและสแกนได้เร็วกว่า เพื่อวิเคราะห์ SQL ที่สร้างขึ้น ให้เปิด logging ในสภาพแวดล้อม development: ```csharp // Program.cs (สำหรับ development เท่านั้น) builder.Services.AddDbContext(options => options.UseSqlServer(connectionString) .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging()); ``` คัดลอก SQL ที่บันทึกไว้ไปยัง SQL Server Management Studio และรันด้วย `SET STATISTICS IO ON` เพื่อตรวจสอบ logical reads หรือใช้ `EXPLAIN ANALYZE` บน PostgreSQL คำแนะนำ index ที่ขาดหายไปจาก query plan มักเผยให้เห็นโอกาสในการเพิ่มประสิทธิภาพที่มีผลกระทบสูงสุด ## Compiled Queries สำหรับ Hot Paths Expression tree ของ LINQ จะถูกแยกวิเคราะห์และแปลเป็น SQL ทุกครั้งที่ดำเนินการ สำหรับ query ที่ทำงานหลายพันครั้งต่อนาที (เช่น การค้นหาการยืนยันตัวตน การตรวจสอบ session) ค่าใช้จ่ายในการแปลนี้จะสะสม Compiled queries จะแคชการแปลไว้ตั้งแต่เริ่มต้นแอปพลิเคชัน ```csharp // CompiledQueries.cs public static class UserQueries { // คอมไพล์ครั้งเดียว ใช้ซ้ำทุกการเรียก public static readonly Func> GetActiveSession = EF.CompileAsyncQuery( (AppDbContext ctx, string token, CancellationToken ct) => ctx.UserSessions .AsNoTracking() .FirstOrDefault(s => s.Token == token && s.ExpiresAt > DateTime.UtcNow)); } // การใช้งานใน middleware var session = await UserQueries.GetActiveSession(dbContext, bearerToken, ct); ``` Compiled queries ข้ามขั้นตอนการแยกวิเคราะห์ expression tree ทั้งหมด ความแตกต่างของประสิทธิภาพจะวัดได้เฉพาะบนเส้นทางความถี่สูง (มากกว่า 1,000 การเรียก/นาที) สำหรับ endpoint CRUD มาตรฐาน แคช query ในตัวของ EF Core จัดการการใช้ซ้ำการแปลอยู่แล้ว ## การแปล Parameterized Collection ใน EF Core 10 Query ที่กรองตามรายการ ID เป็นหนึ่งในรูปแบบที่พบบ่อยที่สุดในการเข้าถึงข้อมูล EF Core 10 เปลี่ยนกลยุทธ์การแปลเริ่มต้นสำหรับ parameterized collections แทนที่จะเข้ารหัสรายการเป็น JSON array (EF Core 8-9) หรือแทรกค่าคงที่แบบ inline (EF Core 7 และก่อนหน้า) EF Core 10 แปลแต่ละค่าเป็นพารามิเตอร์ SQL แยกกัน ```csharp // ก่อนหน้า (EF Core 8-9): พารามิเตอร์ JSON array // @__ids_0='[1,2,3]' // SELECT ... WHERE Id IN (SELECT value FROM OPENJSON(@__ids_0)) // EF Core 10: พารามิเตอร์แยกกันพร้อม padding // SELECT ... WHERE Id IN (@ids1, @ids2, @ids3) var orderIds = new[] { 101, 205, 389 }; var orders = await _context.Orders .Where(o => orderIds.Contains(o.Id)) .ToListAsync(ct); ``` วิธีการใหม่นี้ให้ข้อมูล cardinality แก่ query planner ของฐานข้อมูล ในขณะที่ยังคงหลีกเลี่ยงการบวมของ plan cache ผ่าน parameter padding สำหรับกรณีที่วิธี JSON ทำงานได้ดีกว่า (คอลเลกชันขนาดใหญ่มาก) ให้ override พฤติกรรมต่อ query: ```csharp var orders = await _context.Orders .Where(o => EF.Parameter(orderIds).Contains(o.Id)) // บังคับโหมด JSON .ToListAsync(ct); ``` ## สรุป - ใช้ `AsNoTracking()` และการฉายข้อมูลด้วย `Select` ในทุก query แบบอ่านอย่างเดียวเพื่อขจัดค่าใช้จ่ายของ change tracker - ใช้ `AsSplitQuery()` เมื่อโหลด collection navigation หลายตัวเพื่อหลีกเลี่ยงการระเบิดแบบ Cartesian - แทนที่รูปแบบ tracked load-modify-save ด้วย `ExecuteUpdateAsync` และ `ExecuteDeleteAsync` สำหรับการดำเนินการจำนวนมาก - นำตัวดำเนินการ `LeftJoin` จาก EF Core 10 มาใช้แทนห่วงโซ่ `GroupJoin`/`SelectMany`/`DefaultIfEmpty` ที่ยาวเหยียด - กำหนดค่า named query filters เพื่อจัดการ soft-delete และ multi-tenancy อย่างอิสระ - ตั้งค่า `EnableRetryOnFailure` และปรับขนาด connection pool สำหรับความยืดหยุ่นใน production - กำหนด index แบบผสมและแบบมีตัวกรองตามรูปแบบ query จริง ไม่ใช่การคาดเดา - สงวน compiled queries สำหรับเส้นทางที่ร้อนจริงๆ ที่เกิน 1,000 ครั้งต่อนาที - ปล่อยให้ EF Core 10 จัดการการแปล parameterized collection ตามค่าเริ่มต้น และ override เฉพาะเมื่อ benchmark พิสูจน์ว่าจำเป็น อ่านเพิ่มเติม: [โมดูล EF Core Advanced](/technologies/dotnet/interview-questions/ef-core-advanced) ครอบคลุมรูปแบบเหล่านี้ในบริบทของการสัมภาษณ์ และ [คู่มือ Clean Architecture กับ .NET](/blog/dotnet/clean-architecture-dotnet-practical-guide) สาธิตวิธีการจัดโครงสร้างชั้น repository และ service ที่ห่อหุ้ม query เหล่านี้ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/dotnet/ef-core-performance-best-practices