複雑なクエリ演算子Complex Query Operators

統合言語クエリ (LINQ) には、複数のデータ ソースを結合したり、複雑な処理を行ったりする、多数の複雑な演算子が含まれています。Language Integrated Query (LINQ) contains many complex operators, which combine multiple data sources or does complex processing. すべての LINQ 演算子が、サーバー側で適切に変換されるわけではありません。Not all LINQ operators have suitable translations on the server side. あるフォームのクエリがサーバーに変換されることがありますが、結果が同じであっても、別のフォームで記述されている場合は変換されません。Sometimes, a query in one form translates to the server but if written in a different form doesn't translate even if the result is the same. このページでは、複雑な演算子とそのサポートされるバリエーションについていくつか説明します。This page describes some of the complex operators and their supported variations. 今後のリリースで、さらに多くのパターンを認識し、それらに対応する変換を追加する可能性があります。In future releases, we may recognize more patterns and add their corresponding translations. また、変換のサポートはプロバイダーによって異なることに注意することが重要です。It's also important to keep in mind that translation support varies between providers. SqlServer で変換される特定のクエリは、SQLite データベースでは機能しない場合があります。A particular query, which is translated in SqlServer, may not work for SQLite databases.

ヒント

この記事のサンプルは GitHub で確認できます。You can view this article's sample on GitHub.

JoinJoin

LINQ Join 演算子を使用すると、各ソースのキー セレクターに基づいて 2 つのデータ ソースを接続でき、キーが一致したときに値のタプルが生成されます。The LINQ Join operator allows you to connect two data sources based on the key selector for each source, generating a tuple of values when the key matches. リレーショナル データベースでは、必然的に INNER JOIN に変換されます。It naturally translates to INNER JOIN on relational databases. LINQ Join には外部および内部のキー セレクターがありますが、データベースには単一の結合条件が必要です。While the LINQ Join has outer and inner key selectors, the database requires a single join condition. したがって、EF Core では、外部キー セレクターと内部キー セレクターが等価であるかどうかを比較して、結合条件を生成します。So EF Core generates a join condition by comparing the outer key selector to the inner key selector for equality. さらに、キー セレクターが匿名型である場合、EF Core では、等価要素ごとに比較する結合条件を生成します。Further, if the key selectors are anonymous types, EF Core generates a join condition to compare equality component wise.

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]

GroupJoinGroupJoin

LINQ GroupJoin 演算子では、Join の場合と同じように 2 つのデータ ソースを接続できますが、外部要素と一致するように内部値のグループが作成されます。The LINQ GroupJoin operator allows you to connect two data sources similar to Join, but it creates a group of inner values for matching outer elements. 次の例のようなクエリを実行すると、Blog & IEnumerable<Post> の結果が生成されます。Executing a query like the following example generates a result of Blog & IEnumerable<Post>. データベース (特にリレーショナル データベース) にはクライアント側のオブジェクトのコレクションを表す方法がないため、多くの場合、GroupJoin はサーバーに変換されません。Since databases (especially relational databases) don't have a way to represent a collection of client-side objects, GroupJoin doesn't translate to the server in many cases. サーバーからすべてのデータを取得し、特別なセレクターを使用せずに GroupJoin を実行する必要があります (以下の最初のクエリ)。It requires you to get all of the data from the server to do GroupJoin without a special selector (first query below). しかし、セレクターでデータの選択が制限されている場合は、サーバーからすべてのデータをフェッチすると、パフォーマンスの問題が発生する可能性があります (以下の 2 番目のクエリ)。But if the selector is limiting data being selected then fetching all of the data from the server may cause performance issues (second query below). そのため、EF Core では GroupJoin が変換されません。That's why EF Core doesn't translate GroupJoin.

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

SelectManySelectMany

LINQ SelectMany 演算子を使用すると、外部の各要素のコレクション セレクターを列挙し、各データ ソースから値のタプルを生成することができます。The LINQ SelectMany operator allows you to enumerate over a collection selector for each outer element and generate tuples of values from each data source. ある意味、これは結合ですが、どの条件も含まれていないため、すべての外部要素がコレクション ソースからの要素に接続されます。In a way, it's a join but without any condition so every outer element is connected with an element from the collection source. コレクション セレクターが外部データ ソースにどのように関連しているかによって、SelectMany はサーバー側でさまざまなクエリに変換することができます。Depending on how the collection selector is related to the outer data source, SelectMany can translate into various different queries on the server side.

コレクション セレクターで外部参照されないCollection selector doesn't reference outer

コレクション セレクターで外部ソースから何も参照されていない場合、結果は両方のデータ ソースのデカルト積になります。When the collection selector isn't referencing anything from the outer source, the result is a cartesian product of both data sources. リレーショナル データベースでは CROSS JOIN に変換されます。It translates to CROSS JOIN in relational databases.

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 句でのコレクション セレクターによる外部参照Collection selector references outer in a where clause

コレクション セレクターに where 句があり、外部要素が参照される場合、EF Core ではそれがデータベース結合に変換され、述語が結合条件として使用されます。When the collection selector has a where clause, which references the outer element, then EF Core translates it to a database join and uses the predicate as the join condition. 通常、このケースは、外部要素のコレクション ナビゲーションをコレクション セレクターとして使用する場合に発生します。Normally this case arises when using collection navigation on the outer element as the collection selector. 外部要素のコレクションが空の場合、その外部要素の結果は生成されません。If the collection is empty for an outer element, then no results would be generated for that outer element. しかし、コレクション セレクターに DefaultIfEmpty が適用されている場合、外部要素は内部要素の既定値と接続されます。But if DefaultIfEmpty is applied on the collection selector then the outer element will be connected with a default value of the inner element. このような違いから、この種のクエリは、DefaultIfEmptyLEFT JOIN がない場合、および DefaultIfEmpty が適用されている場合に INNER JOIN に変換されます。Because of this distinction, this kind of queries translates to INNER JOIN in the absence of DefaultIfEmpty and LEFT JOIN when DefaultIfEmpty is applied.

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 以外のケースでのコレクション セレクターによる外部参照Collection selector references outer in a non-where case

コレクション セレクターで、(上記のケースのように) where 句には含まれていない外部要素を参照する場合、データベース結合には変換されません。When the collection selector references the outer element, which isn't in a where clause (as the case above), it doesn't translate to a database join. そのため、外部の各要素のコレクション セレクターを評価する必要があります。That's why we need to evaluate the collection selector for each outer element. 多くのリレーショナル データベースでは APPLY 演算に変換されます。It translates to APPLY operations in many relational databases. 外部要素のコレクションが空の場合、その外部要素の結果は生成されません。If the collection is empty for an outer element, then no results would be generated for that outer element. しかし、コレクション セレクターに DefaultIfEmpty が適用されている場合、外部要素は内部要素の既定値と接続されます。But if DefaultIfEmpty is applied on the collection selector then the outer element will be connected with a default value of the inner element. このような違いから、この種のクエリは、DefaultIfEmptyOUTER APPLY がない場合、および DefaultIfEmpty が適用されている場合に CROSS APPLY に変換されます。Because of this distinction, this kind of queries translates to CROSS APPLY in the absence of DefaultIfEmpty and OUTER APPLY when DefaultIfEmpty is applied. SQLite のような特定のデータベースでは APPLY 演算子がサポートされないため、この種のクエリが変換されない場合があります。Certain databases like SQLite don't support APPLY operators so this kind of query may not be translated.

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]

GroupByGroupBy

LINQ GroupBy 演算子では、IGrouping<TKey, TElement> 型の結果が作成されす。この場合、TKeyTElement は任意の型にすることができます。LINQ GroupBy operators create a result of type IGrouping<TKey, TElement> where TKey and TElement could be any arbitrary type. さらに、IGrouping では IEnumerable<TElement> が実装されます。これは、グループ化の後に LINQ 演算子を使用して、それを構成できることを意味します。Furthermore, IGrouping implements IEnumerable<TElement>, which means you can compose over it using any LINQ operator after the grouping. データベース構造では IGrouping を表すことができないため、ほとんどの場合、GroupBy 演算子は変換されません。Since no database structure can represent an IGrouping, GroupBy operators have no translation in most cases. 各グループに対して、スカラーを返す集約演算子が適用されている場合、リレーショナル データベースでは SQL GROUP BY に変換できます。When an aggregate operator is applied to each group, which returns a scalar, it can be translated to SQL GROUP BY in relational databases. SQL GROUP BY も制限されます。The SQL GROUP BY is restrictive too. スカラー値でのみグループ化する必要があります。It requires you to group only by scalar values. プロジェクションには、列に適用される集約またはグループ化キー列のみを含めることができます。The projection can only contain grouping key columns or any aggregate applied over a column. EF Core では、次の例に示すように、このパターンを識別してサーバーに変換します。EF Core identifies this pattern and translates it to the server, as in the following example:

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 演算子で、グループ化の集約演算子が表示されるクエリを変換します。EF Core also translates queries where an aggregate operator on the grouping appears in a Where or OrderBy (or other ordering) LINQ operator. where 句には、SQL の HAVING 句が使用されます。It uses HAVING clause in SQL for the where clause. GroupBy 演算子を適用する前のクエリの部分は、サーバーに変換できる限り、複雑なクエリにすることができます。The part of the query before applying the GroupBy operator can be any complex query as long as it can be translated to server. さらに、グループ化クエリに集約演算子を適用して、結果のソースからグループ化を削除したら、他のクエリと同様に、それに基づいて構成することができます。Furthermore, once you apply aggregate operators on a grouping query to remove groupings from the resulting source, you can compose on top of it like any other query.

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 でサポートされる集約演算子は次のとおりですThe aggregate operators EF Core supports are as follows

  • AverageAverage
  • CountCount
  • LongCountLongCount
  • MaxMax
  • MinMin
  • SUMSum

Left JoinLeft Join

Left Join は LINQ 演算子ではありませんが、リレーショナル データベースには、クエリで頻繁に使用される Left Join の概念があります。While Left Join isn't a LINQ operator, relational databases have the concept of a Left Join which is frequently used in queries. LINQ クエリの特定のパターンでは、サーバーで LEFT JOIN と同じ結果が得られます。A particular pattern in LINQ queries gives the same result as a LEFT JOIN on the server. EF Core ではこのようなパターンを識別し、サーバー側で同等の LEFT JOIN を生成します。EF Core identifies such patterns and generates the equivalent LEFT JOIN on the server side. このパターンでは、両方のデータソース間に GroupJoin を作成し、その後、内部に関連する要素がない場合は null と一致するように、グループ化ソースに対して DefaultIfEmpty を指定した SelectMany 演算子を使用して、グループ化をフラット化します。The pattern involves creating a GroupJoin between both the data sources and then flattening out the grouping by using the SelectMany operator with DefaultIfEmpty on the grouping source to match null when the inner doesn't have a related element. 次の例では、パターンがどのように見えるかと、何が生成されるかを示します。The following example shows what that pattern looks like and what it generates.

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]

上のパターンでは、式ツリーに複雑な構造体が作成されます。The above pattern creates a complex structure in the expression tree. そのため、EF Core では、演算子の直後のステップで GroupJoin 演算子のグループ化結果をフラット化する必要があります。Because of that, EF Core requires you to flatten out the grouping results of the GroupJoin operator in a step immediately following the operator. GroupJoin-DefaultIfEmpty-SelectMany が使用されていても、パターンが異なる場合は、Left Join として識別されない可能性があります。Even if the GroupJoin-DefaultIfEmpty-SelectMany is used but in a different pattern, we may not identify it as a Left Join.