Entity Framework Core: การเพิ่มประสิทธิภาพและแนวทางปฏิบัติที่ดีที่สุดในปี 2026
คู่มือฉบับสมบูรณ์สำหรับการเพิ่มประสิทธิภาพ Entity Framework Core 10 บน .NET 10 เรียนรู้ AsNoTracking, compiled queries, batch updates, split queries และตัวดำเนินการ LeftJoin

Entity Framework Core 10 ซึ่งเปิดตัวพร้อมกับ .NET 10 LTS ในเดือนพฤศจิกายน 2025 นำเสนอตัวดำเนินการ LeftJoin การค้นหาเวกเตอร์ named query filters และการปรับปรุงการแปล SQL อย่างมีนัยสำคัญ บทความนี้ครอบคลุมแนวทางปฏิบัติที่ดีที่สุดของ EF Core ที่ส่งผลโดยตรงต่อความเร็วในการ query การจัดสรรหน่วยความจำ และความสามารถในการปรับขนาดในสภาพแวดล้อม production
EF Core 10 เป็นเวอร์ชัน Long-Term Support ที่ได้รับการสนับสนุนจนถึงเดือนพฤศจิกายน 2028 ต้องใช้ SDK และ runtime ของ .NET 10 แอปพลิเคชันที่ยังคงใช้ .NET 8 ควรเลือกใช้ EF Core 8 (LTS) จนกว่าจะดำเนินการย้ายระบบเสร็จสิ้น
Query Tracking: เมื่อใดควรปิดการใช้งานและเพราะเหตุใด
ทุกการเรียกใช้ DbSet<T> จะแนบ entity ที่ส่งกลับเข้ากับ change tracker โดยค่าเริ่มต้น Tracker นี้จะเก็บ snapshot ของค่า property เดิม คำนวณความแตกต่างเมื่อเรียก SaveChanges และแก้ไขข้อขัดแย้งของ identity ข้าม navigation ต่างๆ ค่าใช้จ่ายนี้ไม่จำเป็นเมื่อข้อมูลถูกส่งตรงไปยัง API response หรือ view model แบบอ่านอย่างเดียว
public async Task<List<ProductDto>> 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 ที่ไม่เคยแก้ไขข้อมูล ให้ตั้งค่าเริ่มต้นตอนลงทะเบียน:
builder.Services.AddDbContext<CatalogContext>(options =>
options.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));Split Queries เพื่อหลีกเลี่ยงการระเบิดแบบ Cartesian
การโหลด entity ที่มี collection navigation หลายตัวผ่าน Include จะสร้างคำสั่ง SQL เดียวที่มี JOIN เมื่อมีการโหลดสองคอลเลกชันขึ้นไปพร้อมกัน ผลลัพธ์จะขยายตัวเป็นผลคูณ Cartesian ของคอลเลกชันต่างๆ ทำให้ข้อมูลแถวหลักถูกทำซ้ำในทุกการรวมกัน
public async Task<Order?> 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 ยังคงเหมาะสมกว่าเมื่อโหลด 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
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) เหลือเพียงมิลลิวินาที
รูปแบบเดียวกันใช้ได้กับการลบ:
public async Task PurgeExpiredSessionsAsync(CancellationToken ct)
{
await _context.Sessions
.Where(s => s.ExpiresAt < DateTime.UtcNow)
.ExecuteDeleteAsync(ct);
}พร้อมที่จะพิชิตการสัมภาษณ์ .NET แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
ตัวดำเนินการ LeftJoin ใน EF Core 10
ก่อน EF Core 10 การทำ LEFT JOIN ต้องใช้การรวม GroupJoin, SelectMany และ DefaultIfEmpty ในรูปแบบเฉพาะที่นักพัฒนาส่วนใหญ่ต้องค้นหาทุกครั้งที่ใช้งาน EF Core 10 เพิ่ม LeftJoin เป็นตัวดำเนินการ LINQ ระดับหนึ่ง
public async Task<List<EmployeeReportDto>> 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
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Invoice>()
.HasQueryFilter("SoftDelete", i => !i.IsDeleted)
.HasQueryFilter("Tenant", i => i.TenantId == _tenantId);
}Endpoint สำหรับผู้ดูแลระบบสามารถปิด filter soft-delete ในขณะที่ยังคงรักษาการแยก tenant ไว้:
public async Task<List<Invoice>> 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 ในตัว แต่ค่าเริ่มต้นต้องมีการกำหนดค่าอย่างชัดเจน
builder.Services.AddDbContext<AppDbContext>(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 พร้อมกันจริง
Retry อัตโนมัติไม่ทำงานภายใน transaction ที่ผู้ใช้เริ่มต้น ให้ห่อ transaction ทั้งหมดในกลยุทธ์ retry แบบ manual โดยใช้ context.Database.CreateExecutionStrategy().ExecuteAsync(...) เพื่อจัดการข้อผิดพลาดชั่วคราวในหลายการดำเนินการ
กลยุทธ์การทำ Indexing และการวิเคราะห์ Query
Migration ของ EF Core สามารถกำหนด index แบบประกาศได้ แต่การเลือกคอลัมน์ที่จะทำ index ต้องอาศัยความเข้าใจรูปแบบ query
modelBuilder.Entity<Order>(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:
builder.Services.AddDbContext<AppDbContext>(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 จะแคชการแปลไว้ตั้งแต่เริ่มต้นแอปพลิเคชัน
public static class UserQueries
{
// คอมไพล์ครั้งเดียว ใช้ซ้ำทุกการเรียก
public static readonly Func<AppDbContext, string, CancellationToken, Task<UserSession?>>
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 แยกกัน
// ก่อนหน้า (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:
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 ครอบคลุมรูปแบบเหล่านี้ในบริบทของการสัมภาษณ์ และ คู่มือ Clean Architecture กับ .NET สาธิตวิธีการจัดโครงสร้างชั้น repository และ service ที่ห่อหุ้ม query เหล่านี้
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

.NET 10 ในปี 2026: ฟีเจอร์ใหม่ Native AOT และคำถามสัมภาษณ์งานที่ต้องรู้
สรุปฟีเจอร์ใหม่ของ .NET 10 LTS ครอบคลุม Native AOT, C# 14 extension members, field keyword และ EF Core 10 พร้อมคำถามสัมภาษณ์งานที่พบบ่อยในปี 2026

.NET MAUI ในปี 2026: การพัฒนา Cross-Platform และคำถามสัมภาษณ์งาน
บทความสอน .NET MAUI ครอบคลุมการพัฒนา cross-platform ด้วย .NET 10, handler architecture, MVVM, HybridWebView และคำถามสัมภาษณ์งานที่สำคัญในปี 2026

.NET 9 Blazor: พัฒนาแอปพลิเคชัน Full-Stack ด้วย Blazor United ปี 2026
Blazor United ใน .NET 9 ผสานรวม Static SSR, Interactive Server และ WebAssembly ไว้ในสถาปัตยกรรมเดียว บทความนี้อธิบายโหมดการ render ทั้ง 4 แบบ, streaming rendering, dependency injection, AOT compilation และแนวทางปฏิบัติสำหรับ production อย่างครบถ้วน