Start Debugging

EF Core 11 が split query で不要な reference join を刈り込む

EF Core 11 Preview 3 は split query から冗長な to-one join を除去し、不要な ORDER BY キーを落とします。報告された一つのシナリオは 29% 速くなり、別のは 22% でした。今の SQL はこう見えます。

EF Core の split query には常に鋭い角がありました: reference navigation の Include と collection navigation の Include を混ぜると、collection query 側で何も必要としていないのに、すべての子 query が reference テーブルを再 join していました。EF Core 11 Preview 3 はこれを修正し、関連する ORDER BY 過剰指定も一緒に直します。release notes は benchmark 影響を一般的な split-query シナリオで 29%、single-query ケースで 22% と計上します。LINQ の編集なしで本番に現れる類の変更です。

不要だった余分な join

定型を考えます: to-one の BlogType と to-many の Posts を持つブログを AsSplitQuery() でロード:

var blogs = context.Blogs
    .Include(b => b.BlogType)
    .Include(b => b.Posts)
    .AsSplitQuery()
    .ToList();

split query は include された collection ごとに 1 つの SQL とルート query を実行します。ルート query は BlogType のカラムを投影するために正当に join が必要です。Posts の collection query はそうではありません。post のカラムしか投影しないからです。EF Core 10 以前もそれでも join を発行していました:

-- Before EF Core 11
SELECT [p].[Id], [p].[BlogId], [p].[Title], [b].[Id], [b0].[Id]
FROM [Blogs] AS [b]
INNER JOIN [BlogType] AS [b0] ON [b].[BlogTypeId] = [b0].[Id]
INNER JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id], [b0].[Id]

その余分な INNER JOIN [BlogType] は全行で解決され、ソートにも参加します。payload 的な理由はまったくありません。EF Core 11 はそれを刈り取ります:

-- EF Core 11
SELECT [p].[Id], [p].[BlogId], [p].[Title], [b].[Id]
FROM [Blogs] AS [b]
INNER JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]

Include に束ねた reference navigation が多いほど、消える join も多くなります。ドメインモデルが本物の collection と並んで小さな lookup (CountryStatusCurrency) の Include に頼っているなら、これは本質的にタダのスループットです。

ORDER BY 過剰指定も消える

2 つ目の最適化は single query にも適用されます。reference navigation を include すると、親の primary key が foreign key 経由ですでにそれを決定しているのに、EF は歴史的にその key を ORDER BY 節に発行していました:

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

EF Core 11 以前:

ORDER BY [b].[BlogId], [p].[PersonId]

EF Core 11 では:

ORDER BY [b].[BlogId]

BlogId は一意であり、PersonId は FK 経由で BlogId によって完全に決定されていたので、それを sort key に保持するのは純粋なコストでした。それを落とすと sort key が短くなり、それはテーブルが十分大きくなってディスクにスピルするようになったり、planner が結果上に merge join を選んだりするときに重要になります。

いつ気づくか

最も大きな勝ちは、複数の小さな reference include と 1 つ以上の collection include を持つ query で見られます。そういった query はすべての子 query で同じ不要な join を繰り返していたからです。Customer-order、invoice-with-lines、blog-with-posts が明白な候補です。AsSplitQuery() なしの query や reference include なしの query は ORDER BY 簡略化は得られますが、join 刈り込みは得られません。

API 変更はなく、オンにするものもありません。EF Core 11.0.0-preview.3 (.NET 11 Preview 3 をターゲット) にアップグレードし、同じ LINQ を走らせると、生成される SQL がタイトになります。benchmark 詳細は EF Core のトラッキング issue にあります。

< 戻る