다음을 통해 공유


복합 쿼리 연산자

LINQ(언어 통합 쿼리)에는 여러 데이터 원본을 결합하거나 복잡한 처리를 수행하는 여러 복합 연산자가 포함되어 있습니다. 모든 LINQ 연산자가 서버 쪽에서 적절한 번역을 사용하는 것은 아닙니다. 경우에 따라 한 폼의 쿼리가 서버로 변환되지만 다른 형식으로 작성된 경우 결과가 같더라도 번역되지 않습니다. 이 페이지에서는 일부 복합 연산자와 지원되는 변형에 대해 설명합니다. 이후 릴리스에서는 더 많은 패턴을 인식하고 해당 번역을 추가할 수 있습니다. 또한 번역 지원은 공급자마다 다릅니다. SqlServer에서 번역된 특정 쿼리는 SQLite 데이터베이스에서 작동하지 않을 수 있습니다.

팁 (조언)

GitHub에서 이 문서의 샘플을 볼 수 있습니다.

참여

LINQ 조인 연산자를 사용하면 각 원본의 키 선택기를 기반으로 두 개의 데이터 원본을 연결하여 키가 일치할 때 값의 튜플을 생성할 수 있습니다. 자연스럽게 관계형 데이터베이스로 INNER JOIN 변환됩니다. LINQ 조인에는 외부 및 내부 키 선택기가 있지만 데이터베이스에는 단일 조인 조건이 필요합니다. 따라서 EF Core는 외부 키 선택기를 내부 키 선택기와 동등하게 비교하여 조인 조건을 생성합니다.

var query = from photo in context.Set<PersonPhoto>()
            join person in context.Set<Person>()
                on photo.PersonPhotoId equals person.PhotoId
            select new { person, photo };
SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo]
FROM [PersonPhoto] AS [p0]
INNER JOIN [Person] AS [p] ON [p0].[PersonPhotoId] = [p].[PhotoId]

또한 키 선택기가 익명 형식인 경우 EF Core는 같음 구성 요소별 비교를 위한 조인 조건을 생성합니다.

var query = from photo in context.Set<PersonPhoto>()
            join person in context.Set<Person>()
                on new { Id = (int?)photo.PersonPhotoId, photo.Caption }
                equals new { Id = person.PhotoId, Caption = "SN" }
            select new { person, photo };
SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo]
FROM [PersonPhoto] AS [p0]
INNER JOIN [Person] AS [p] ON ([p0].[PersonPhotoId] = [p].[PhotoId] AND ([p0].[Caption] = N'SN'))

그룹조인

LINQ GroupJoin 연산자를 사용하면 Join과 비슷한 두 데이터 원본을 연결할 수 있지만 외부 요소를 일치시키는 내부 값 그룹을 만듭니다. 다음 예제와 같이 쿼리를 실행하면 BlogIEnumerable<Post>의 결과가 생성됩니다. 데이터베이스(특히 관계형 데이터베이스)에는 클라이언트 쪽 개체의 컬렉션을 나타내는 방법이 없으므로 GroupJoin은 대부분의 경우 서버로 변환되지 않습니다. 특수 선택기 없이 GroupJoin을 수행하려면 서버에서 모든 데이터를 가져와야 합니다(아래 첫 번째 쿼리). 그러나 선택기가 선택 중인 데이터를 제한하는 경우 서버에서 모든 데이터를 페치하면 성능 문제가 발생할 수 있습니다(아래 두 번째 쿼리). EF Core가 GroupJoin을 번역하지 않는 이유입니다.

var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            select new { b, grouping };
var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            select new { b, Posts = grouping.Where(p => p.Content.Contains("EF")).ToList() };

SelectMany

LINQ SelectMany 연산자를 사용하면 각 외부 요소에 대한 컬렉션 선택기를 열거하고 각 데이터 원본에서 값의 튜플을 생성할 수 있습니다. 어떤 면에서는 조인이지만 조건이 없으므로 모든 외부 요소가 컬렉션 원본의 요소와 연결됩니다. 컬렉션 선택기가 외부 데이터 원본과 관련된 방식에 따라 SelectMany는 서버 쪽에서 다양한 쿼리로 변환할 수 있습니다.

컬렉션 선택기가 외부를 참조하지 않음

컬렉션 선택기가 외부 원본에서 아무것도 참조하지 않는 경우 결과는 두 데이터 원본의 카티전 곱입니다. 관계형 데이터베이스에서는 CROSS JOIN로 번역됩니다.

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>()
            select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
CROSS JOIN [Posts] AS [p]

where 절의 외부를 참조하는 컬렉션 선택기

컬렉션 선택기에서 외부 요소를 참조하는 where 절이 있는 경우 EF Core는 이를 데이터베이스 조인으로 변환하고 조건자를 조인 조건으로 사용합니다. 일반적으로 이 경우는 외부 요소에서 컬렉션 탐색을 컬렉션 선택기로 사용할 때 발생합니다. 외부 요소에 대해 컬렉션이 비어 있으면 해당 외부 요소에 대한 결과가 생성되지 않습니다. 그러나 컬렉션 선택기에서 적용되는 경우 DefaultIfEmpty 외부 요소는 내부 요소의 기본값으로 연결됩니다. 이러한 구분 때문에, INNER JOIN가 적용될 때 DefaultIfEmptyLEFT JOIN가 없는 경우에 이러한 종류의 쿼리는 DefaultIfEmpty로 변환됩니다.

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId)
            select new { b, p };

var query2 = from b in context.Set<Blog>()
             from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId).DefaultIfEmpty()
             select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

컬렉션 선택기는 비 WHERE 절 상태에서 외부 쿼리를 참조합니다.

컬렉션 선택기가 where 절에 없는 외부 요소를 참조하는 경우(위의 경우와 같이) 데이터베이스 조인으로 변환되지 않습니다. 따라서 각 외부 요소에 대한 컬렉션 선택기를 평가해야 합니다. APPLY 많은 관계형 데이터베이스에서 작업으로 변환됩니다. 외부 요소에 대해 컬렉션이 비어 있으면 해당 외부 요소에 대한 결과가 생성되지 않습니다. 그러나 컬렉션 선택기에서 적용되는 경우 DefaultIfEmpty 외부 요소는 내부 요소의 기본값으로 연결됩니다. 이러한 구분 때문에, CROSS APPLY가 적용될 때 DefaultIfEmptyOUTER APPLY가 없는 경우에 이러한 종류의 쿼리는 DefaultIfEmpty로 변환됩니다. SQLite와 같은 특정 데이터베이스는 연산자를 지원하지 APPLY 않으므로 이러한 종류의 쿼리는 변환되지 않을 수 있습니다.

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>().Select(p => b.Url + "=>" + p.Title)
            select new { b, p };

var query2 = from b in context.Set<Blog>()
             from p in context.Set<Post>().Select(p => b.Url + "=>" + p.Title).DefaultIfEmpty()
             select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], ([b].[Url] + N'=>') + [p].[Title] AS [p]
FROM [Blogs] AS [b]
CROSS APPLY [Posts] AS [p]

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], ([b].[Url] + N'=>') + [p].[Title] AS [p]
FROM [Blogs] AS [b]
OUTER APPLY [Posts] AS [p]

그룹별 정렬

LINQ GroupBy 연산자는 IGrouping<TKey, TElement>TKey가 임의의 형식일 수 있는 TElement 형식의 결과를 생성합니다. 또한 IGroupingIEnumerable<TElement>을 구현하므로 그룹화 후 LINQ 연산자를 사용하여 구성할 수 있습니다. 데이터베이스 구조를 나타낼 IGrouping수 없으므로 GroupBy 연산자는 대부분의 경우 변환이 없습니다. 스칼라를 반환하는 집계 연산자를 각 그룹에 적용하면 관계형 데이터베이스에서 SQL GROUP BY 로 변환할 수 있습니다. SQL GROUP BY 도 제한적입니다. 스칼라 값으로만 그룹화해야 합니다. 프로젝션에는 그룹화 키 열 또는 열에 적용된 집계만 포함될 수 있습니다. EF Core는 다음 예제와 같이 이 패턴을 식별하고 서버로 변환합니다.

var query = from p in context.Set<Post>()
            group p by p.AuthorId
            into g
            select new { g.Key, Count = g.Count() };
SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count]
FROM [Posts] AS [p]
GROUP BY [p].[AuthorId]

또한 EF Core는 그룹화의 집계 연산자가 Where 또는 OrderBy(또는 기타 순서 지정) LINQ 연산자에 표시되는 쿼리를 변환합니다. SQL에서 HAVING 절을 WHERE 절에 사용합니다. GroupBy 연산자를 적용하기 전에 쿼리의 부분은 서버로 변환할 수 있는 한 복잡한 쿼리일 수 있습니다. 또한 그룹화 쿼리에 집계 연산자를 적용하여 결과 원본에서 그룹화가 제거되면 다른 쿼리와 마찬가지로 그 위에 작성할 수 있습니다.

var query = from p in context.Set<Post>()
            group p by p.AuthorId
            into g
            where g.Count() > 0
            orderby g.Key
            select new { g.Key, Count = g.Count() };
SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count]
FROM [Posts] AS [p]
GROUP BY [p].[AuthorId]
HAVING COUNT(*) > 0
ORDER BY [p].[AuthorId]

EF Core에서 지원하는 집계 연산자는 다음과 같습니다.

닷넷 SQL (영문)
Average(x => x.Property) AVG(속성)
카운트() COUNT(*)
LongCount() COUNT(*)
Max(x => x.Property) MAX(속성)
Min(x => x.속성) MIN(속성)
합계(x => x.Property) SUM(Property)

추가 집계 연산자가 지원될 수 있습니다. 공급자 문서에서 더 많은 함수 매핑을 확인합니다.

나타낼 IGrouping데이터베이스 구조가 없더라도 경우에 따라 EF Core 7.0 이상에서는 데이터베이스에서 결과가 반환된 후 그룹화할 수 있습니다. 이는 관련 컬렉션을 포함할 때 연산자가 Include 작동하는 방식과 유사합니다. 다음 LINQ 쿼리는 GroupBy 연산자를 사용하여 결과를 Price 속성 값으로 그룹화합니다.

var query = context.Books.GroupBy(s => s.Price);
SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]

이 경우 GroupBy 연산자는 SQL 절로 직접적으로 변환되지 않으며, 대신 EF Core가 서버에서 결과를 받은 후에 그룹화를 수행합니다.

왼쪽 조인

왼쪽 조인은 LINQ 연산자가 아니지만 관계형 데이터베이스에는 쿼리에서 자주 사용되는 왼쪽 조인의 개념이 있습니다. LINQ 쿼리의 특정 패턴은 서버의 결과와 LEFT JOIN 동일한 결과를 제공합니다. EF Core는 이러한 패턴을 식별하고 서버 쪽에서 해당 LEFT JOIN 패턴을 생성합니다. 패턴에는 두 데이터 원본 간에 GroupJoin을 만든 다음 그룹화 원본에서 DefaultIfEmpty와 함께 SelectMany 연산자를 사용하여 그룹화가 평면화되어 내부가 관련 요소가 없는 경우 null과 일치하도록 하는 패턴이 포함됩니다. 다음 예제에서는 해당 패턴의 모양과 패턴이 생성하는 항목을 보여 줍니다.

var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            from p in grouping.DefaultIfEmpty()
            select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

위의 패턴은 식 트리에 복잡한 구조를 만듭니다. 따라서 EF Core를 사용하려면 운영자 바로 다음 단계에서 GroupJoin 연산자의 그룹화 결과를 평면화해야 합니다. GroupJoin-DefaultIfEmpty-SelectMany이 사용되더라도, 다른 패턴으로 사용하면 왼쪽 조인으로 식별되지 않을 수 있습니다.