複雑なクエリ演算子

統合言語クエリ (LINQ) には、複数のデータ ソースを結合したり、複雑な処理を行ったりする、多数の複雑な演算子が含まれています。 すべての LINQ 演算子が、サーバー側で適切に変換されるわけではありません。 あるフォームのクエリがサーバーに変換されることがありますが、結果が同じであっても、別のフォームで記述されている場合は変換されません。 このページでは、複雑な演算子とそのサポートされるバリエーションについていくつか説明します。 今後のリリースで、さらに多くのパターンを認識し、それらに対応する変換を追加する可能性があります。 また、変換のサポートはプロバイダーによって異なることに注意することが重要です。 SqlServer で変換される特定のクエリは、SQLite データベースでは機能しない場合があります。

ヒント

この記事のサンプルは GitHub で確認できます。

参加する

LINQ Join 演算子を使用すると、各ソースのキー セレクターに基づいて 2 つのデータ ソースを接続でき、キーが一致したときに値のタプルが生成されます。 リレーショナル データベースでは、必然的に INNER JOIN に変換されます。 LINQ Join には外部および内部のキー セレクターがありますが、データベースには単一の結合条件が必要です。 したがって、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'))

GroupJoin

LINQ GroupJoin 演算子では、Join の場合と同じように 2 つのデータ ソースを接続できますが、外部要素と一致するように内部値のグループが作成されます。 次の例のようなクエリを実行すると、Blog および IEnumerable<Post> という結果が生成されます。 データベース (特にリレーショナル データベース) にはクライアント側のオブジェクトのコレクションを表す方法がないため、多くの場合、GroupJoin はサーバーに変換されません。 サーバーからすべてのデータを取得し、特別なセレクターを使用せずに GroupJoin を実行する必要があります (以下の最初のクエリ)。 しかし、セレクターでデータの選択が制限されている場合は、サーバーからすべてのデータをフェッチすると、パフォーマンスの問題が発生する可能性があります (以下の 2 番目のクエリ)。 そのため、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 が適用されている場合、外部要素は内部要素の既定値と接続されます。 このような違いから、この種のクエリは、DefaultIfEmptyLEFT JOIN がない場合、および DefaultIfEmpty が適用されている場合に INNER JOIN に変換されます。

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 が適用されている場合、外部要素は内部要素の既定値と接続されます。 このような違いから、この種のクエリは、DefaultIfEmptyOUTER APPLY がない場合、および DefaultIfEmpty が適用されている場合に CROSS APPLY に変換されます。 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]

GroupBy

LINQ GroupBy 演算子では、IGrouping<TKey, TElement> 型の結果が作成されす。この場合、TKeyTElement は任意の型にすることができます。 さらに、IGrouping では IEnumerable<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 演算子で、グループ化の集約演算子が表示されるクエリを変換します。 where 句には、SQL の HAVING 句が使用されます。 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 でサポートされる集約演算子は次のとおりです

.NET SQL
Average(x => x.Property) AVG(Property)
Count() COUNT(*)
LongCount() COUNT(*)
Max(x => x.Property) MAX(Property)
Min(x => x.Property) MIN(Property)
Sum(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 の GROUP BY 句に直接変換されませんが、代わりに、サーバーから結果が返された後に EF Core によってグループ化が作成されます。

左結合

Left Join は LINQ 演算子ではありませんが、リレーショナル データベースには、クエリで頻繁に使用される Left Join の概念があります。 LINQ クエリの特定のパターンでは、サーバーで LEFT JOIN と同じ結果が得られます。 EF Core ではこのようなパターンを識別し、サーバー側で同等の LEFT JOIN を生成します。 このパターンでは、両方のデータソース間に GroupJoin を作成し、その後、内部に関連する要素がない場合は null と一致するように、グループ化ソースに対して DefaultIfEmpty を指定した SelectMany 演算子を使用して、グループ化をフラット化します。 次の例では、パターンがどのように見えるかと、何が生成されるかを示します。

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 が使用されていても、パターンが異なる場合は、Left Join として識別されない可能性があります。