본문 바로가기
Entity Framework Core

.NET Core MVC] EF Core - 10. Cartesian Product, Cartesian Explosion

by Fastlane 2023. 9. 4.
728x90
반응형

Single query의 Performance issue

RDB 작업을 할때, EF는 single query에 JOIN을 사용해서 related entities 데이터를 로딩한다. SQL에서 JOIN이 일반적인 방법이지만, 부적절하게 사용하면 심각한 Performance issue를 일으킬 수 있다. 

 

Cartesian Product

Cartesian product는 2 sets의 곱을 나타내는 수학적 표현이다. first set이 2 elements로 이루어져 있고, second set이 3 elements로 이루어져있을 때, cartesian product 결과는 6 elements이다. first set의 각 elements에 second set의 모든 elements를 조합한다. 

 

{ a1, a2 } x { b1, b2, b3 } = { a1b1, a1b2, a1b3, a2b1, a2b2, a2b3 }

 

Cartesian Explosion

Blog의 collection navigation으로 Posts, Contributors가 있으면 둘 다 동일한 level이기 떄문에 relational database는 cross product를 반환한다. Posts의 각 행은 Contributors의 각 행과 join된다. 즉, 한 blog가 10개의 posts와 10 contributors를 갖는다면, db는 하나의 blog에 대해 100 rows를 반환한다. 이 현상을 cartesian explosion이라고 하며, 의도치 않는 거대한 양의 data를 client에 전달하게 된다. 

var coursesJoin = _context.Courses
    .Include(c => c.Enrollments)
    .Include(c => c.CourseAssignments)
    .AsNoTracking();

return View(await coursesJoin.ToListAsync());

SQL

  SELECT [c].[CourseID], [c].[Credits], [c].[DepartmentID], [c].[Title], [e].[EnrollmentID], [e].[CourseID], [e].[Grade], [e].[StudentID], [c0].[CourseID], [c0].[InstructorID]
  FROM [Course] AS [c]
  LEFT JOIN [Enrollment] AS [e] ON [c].[CourseID] = [e].[CourseID]
  LEFT JOIN [CourseAssignment] AS [c0] ON [c].[CourseID] = [c0].[CourseID]
  ORDER BY [c].[CourseID], [e].[EnrollmentID], [c0].[CourseID]

데이터 중복

JOIN은 또 다른 performance issue를 일으킨다. 아래 쿼리는 하나의 collection navigation을 로딩한다. 

 

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ToList();

SQL

SELECT [b].[Id], [b].[Name], [b].[HugeColumn], [p].[Id], [p].[BlogId], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]

blog properties는 blog가 가진 모든 post에 대해 중복된다. 이것이 일반적인 일로 문제가 되지 않지만, Blog table이 큰 column을 갖는 경우 트래픽 이슈가 있을 수 있다. 

 

큰 값의 column이 필요하지 않으면, 해당 컬럼은 쿼리 항목에서 제외한다. 

var blogs = ctx.Blogs
    .Select(b => new
    {
        b.Id,
        b.Name,
        b.Posts
    })
    .ToList();

projection을 이용해서, performance 향상을 위해 큰 용량의 컬럼은 제외하고 필요한 컬럼만 선택한다. collection navigation을 로딩하지 않더라도, 필요한 컬럼만 projection하는 것이 좋다. 하지만 익명 타입으로 프로젝트 하기 때문에, EF에 의해서 track되지 않는다. 값을 변경해도 저장되지 않는다. 

 

일반적으로 중복 데이터 사이즈는 무시되며, 큰 컬럼에 한해서 주의해야 한다. 

 Split queries

앞의 performance issue를 해결하기 위해, EF는 LINQ query를 여러개의 SQL queries로 나누는 것을 지정하도록 허용한다. JOIN 대신에, 분할 쿼리는 각 included collection navigation에 대해 추가 SQL을 생성한다. 

var coursesJoin = _context.Courses
    .Include(c => c.Enrollments)
    .Include(c => c.CourseAssignments)
    .AsSplitQuery()
    .AsNoTracking();

SQL

  SELECT [c].[CourseID], [c].[Credits], [c].[DepartmentID], [c].[Title]
  FROM [Course] AS [c]
  ORDER BY [c].[CourseID]

  SELECT [e].[EnrollmentID], [e].[CourseID], [e].[Grade], [e].[StudentID], [c].[CourseID]
  FROM [Course] AS [c]
  INNER JOIN [Enrollment] AS [e] ON [c].[CourseID] = [e].[CourseID]
  ORDER BY [c].[CourseID]

  SELECT [c0].[CourseID], [c0].[InstructorID], [c].[CourseID]
  FROM [Course] AS [c]
  INNER JOIN [CourseAssignment] AS [c0] ON [c].[CourseID] = [c0].[CourseID]
  ORDER BY [c].[CourseID]

분할 쿼리의 특징

조인 및 Cartesian Explosion 관련 성능 문제를 방지하지만, 몇가지 단점이 있다. 

  • 여러 쿼리는 데이터 일관성 보장이 되지 않는다. 
  • 각 쿼리는 DB에 대한 추가 네트워크 왕복을 의미한다. 
  • 대부분의  DB에서는 지정된 시점에 단일 쿼리만 활성화될 수 있다. 따라서 메모리 요구사항이 늘어난다. 

모든 시나리오에 적합한 related entity를 로드하는 하나의 전략은 없다. single query, split query 장단점을 고려해여 적합한 쿼리를 선택해야 한다. 

728x90
반응형

댓글