C#.NET で LINQ ク゚リを最適化する方法

導入

В この蚘事 いく぀かの最適化方法が怜蚎されたした LINQク゚リ.
ここでは、以䞋に関連するコヌド最適化ぞのいく぀かのアプロヌチも玹介したす。 LINQク゚リ.

それが知られおいる LINQ(蚀語統合ク゚リ) は、デヌタ ゜ヌスをク゚リするためのシンプルで䟿利な蚀語です。

А LINQ to SQL DBMS 内のデヌタにアクセスするためのテクノロゞです。これはデヌタを操䜜するための匷力なツヌルであり、ク゚リは宣蚀型蚀語を通じお構築され、その埌、次のように倉換されたす。 SQLク゚リ プラットフォヌムに送信され、実行のためにデヌタベヌス サヌバヌに送信されたす。この堎合、DBMS ずは MS SQL Server.

しかし、 LINQク゚リ 最適に曞かれたものに倉換されない SQLク゚リ、経隓豊富な DBA であれば、最適化のあらゆるニュアンスを含めお曞くこずができたす。 SQL ク゚リ:

  1. 最適な接続 (登録) および結果のフィルタリング (WHERE)
  2. 接続ずグルヌプ条件の䜿甚における倚くのニュアンス
  3. 亀換条件のバリ゚ヌションが豊富 IN Ма 存圚するО ありたせんで、<> 侊 存圚する
  4. 䞀時テヌブル、CTE、テヌブル倉数による結果の䞭間キャッシュ
  5. 文の䜿甚 (オプション) 説明ず衚のヒント付き WITH ...
  6. 遞択䞭の冗長なデヌタ読み取りを取り陀く手段の XNUMX ぀ずしおむンデックス付きビュヌを䜿甚する

結果ずしお生じる䞻なパフォヌマンスのボトルネック SQL ク゚リ コンパむル時 LINQク゚リ 次のずおりです。

  1. デヌタ遞択メカニズム党䜓を XNUMX ぀のリク゚ストに統合
  2. 同䞀のコヌド ブロックを耇補し、最終的には耇数の䞍芁なデヌタの読み取りに぀ながる
  3. 耇数の芁玠からなる条件のグルヌプ (論理的な「and」ず「or」) - そしお О ORを耇雑な条件に組み合わせるず、オプティマむザが必芁なフィヌルドに適切な非クラスタヌ化むンデックスを持ち、最終的にクラスタヌ化むンデックスに察しおスキャンを開始するずいう事実が生じたす (むンデックススキャン) 条件のグルヌプごず
  4. サブク゚リのネストが深いため、解析が非垞に問題になりたす SQL文 開発者偎のク゚リ プランの分析ず DBA

最適化手法

それでは、最適化メ゜ッドに盎接移りたしょう。

1) 远加のむンデックス䜜成

倚くの堎合、ク゚リ党䜓が XNUMX ぀たたは XNUMX ぀のメむン テヌブル (アプリケヌション、ナヌザヌ、操䜜) を䞭心に、暙準の条件セット (IsClosed、Canceled、Enabled、Status) を䜿甚しお構築されるため、メむンの遞択テヌブルでフィルタヌを怜蚎するこずをお勧めしたす。特定されたサンプルに察しお適切なむンデックスを䜜成するこずが重芁です。

これらのフィヌルドを遞択するず、ク゚リに返されるセットが倧幅に制限される堎合、この゜リュヌションは意味がありたす。

たずえば、500000 件のアプリケヌションがありたす。ただし、アクティブなアプリケヌションは 2000 件しかありたせん。次に、正しく遞択されたむンデックスにより、次のような問題が発生したす。 むンデックススキャン 倧きなテヌブルの堎合は、非クラスタヌ化むンデックスを䜿甚しおデヌタをすばやく遞択できたす。

たた、むンデックスの䞍足は、ク゚リ プランの解析たたはシステム ビュヌ統蚈の収集を求めるプロンプトによっお特定できたす。 MS SQL Server:

  1. sys.dm_db_missing_index_groups
  2. sys.dm_db_missing_index_group_stats
  3. sys.dm_db_missing_index_details

空間むンデックスを陀き、すべおのビュヌ デヌタには、欠萜しおいるむンデックスに関する情報が含たれおいたす。

ただし、むンデックスずキャッシュは、倚くの堎合、䞍適切に蚘述された堎合の圱響に察凊する方法です。 LINQク゚リ О SQL ク゚リ.

厳しい生掻習慣が瀺すように、倚くの堎合、䌁業にずっお、特定の期限たでにビゞネス機胜を実装するこずが重芁です。したがっお、重いリク゚ストはキャッシュを䜿甚しおバックグラりンドに転送されるこずがよくありたす。

ナヌザヌは必ずしも最新のデヌタを必芁ずするわけではなく、ナヌザヌ むンタヌフェむスの応答性には蚱容可胜なレベルがあるため、これは郚分的には正圓化されたす。

このアプロヌチにより、ビゞネス ニヌズの解決は可胜になりたすが、問題の解決が遅れるだけで、最終的には情報システムのパフォヌマンスが䜎䞋したす。

远加する必芁があるむンデックスを怜玢する過皋で、提案が行われるこずも芚えおおく䟡倀がありたす。 MS SQL 次のような条件䞋では、最適化が正しく行われない可胜性がありたす。

  1. 同様のフィヌルドのセットを持぀むンデックスがすでに存圚する堎合
  2. むンデックス䜜成の制限によりテヌブル内のフィヌルドにむンデックスを䜜成できない堎合 (詳现は埌述) ここで).

2) 属性を XNUMX ぀の新しい属性に結合する

堎合によっおは、条件グルヌプの基瀎ずしお機胜する XNUMX ぀のテヌブルの䞀郚のフィヌルドを、XNUMX ぀の新しいフィヌルドを導入するこずで眮き換えるこずができたす。

これは、ステヌタス フィヌルドに特に圓おはたりたす。ステヌタス フィヌルドは通垞、ビット型たたは敎数型です。

䟋

IsClosed = 0 か぀キャンセル枈み = 0 か぀有効 = 0 に眮き換えられる ステヌタス = 1.

ここでは、これらのステヌタスがテヌブルに確実に入力されるようにするために、敎数の Status 属性が導入されおいたす。次に、この新しい属性にむンデックスが付けられたす。

䞍必芁な蚈算を行わずにデヌタにアクセスするため、これはパフォヌマンスの問題に察する根本的な解決策です。

3) ビュヌの具䜓化

残念ながら、 LINQク゚リ 䞀時テヌブル、CTE、テヌブル倉数は盎接䜿甚できたせん。

ただし、この堎合に最適化する別の方法、぀たりむンデックス付きビュヌがありたす。

条件グルヌプ (䞊蚘の䟋から) IsClosed = 0 か぀キャンセル枈み = 0 か぀有効 = 0 (たたは他の同様の条件のセット) は、むンデックス付きビュヌでそれらを䜿甚し、倧芏暡なセットからデヌタの小さなスラむスをキャッシュするための良いオプションになりたす。

ただし、ビュヌを実䜓化する堎合には、いく぀かの制限がありたす。

  1. サブク゚リ、句の䜿甚 存圚する を䜿甚しお眮き換える必芁がありたす 登録
  2. 文章は䜿えない 連合, UNION ALL, 䟋倖, 亀差する
  3. テヌブルのヒントず句は䜿甚できたせん オプション
  4. サむクルを扱う可胜性はありたせん
  5. 異なるテヌブルのデヌタを XNUMX ぀のビュヌに衚瀺するこずはできたせん

むンデックス付きビュヌを䜿甚する本圓のメリットは、実際にむンデックスを䜜成するこずによっおのみ埗られるこずを芚えおおくこずが重芁です。

ただし、ビュヌを呌び出す堎合、これらのむンデックスは䜿甚できない堎合があり、明瀺的に䜿甚するには、次のように指定する必芁がありたす。 あり(拡匵なし).

以来 LINQク゚リ テヌブル ヒントを定矩するこずは䞍可胜なので、別の衚珟、぀たり次の圢匏の「ラッパヌ」を䜜成する必芁がありたす。

CREATE VIEW ИМЯ_преЎставлеМОя AS SELECT * FROM MAT_VIEW WITH (NOEXPAND);

4) テヌブル関数の䜿甚

しばしば LINQク゚リ サブク゚リの倧きなブロック、たたは耇雑な構造のビュヌを䜿甚するブロックは、非垞に耇雑で次善の実行構造を持぀最終的なク゚リを圢成したす。

テヌブル関数を䜿甚する䞻な利点 LINQク゚リ:

  1. ビュヌの堎合ず同様に、オブゞェクトずしお䜿甚および指定できる機胜ですが、䞀連の入力パラメヌタを枡すこずもできたす。
    FROM FUNCTION(@param1, @param2 ...)
    これにより、柔軟なデヌタサンプリングが可胜になりたす。
  2. テヌブル関数を䜿甚する堎合、䞊蚘のむンデックス付きビュヌの堎合のような匷い制限はありたせん。
    1. テヌブルのヒント:
      スルヌ LINQ ク゚リ時に䜿甚するむンデックスを指定したり、デヌタ分離レベルを決定したりするこずはできたせん。
      しかし、この関数にはこれらの機胜がありたす。
      この関数を䜿甚するず、むンデックスずデヌタ分離レベルを操䜜するためのルヌルが定矩されおいる、ほが䞀定の実行ク゚リ プランを実珟できたす。
    2. この関数を䜿甚するず、むンデックス付きビュヌず比范しお、以䞋を取埗できたす。
      • 耇雑なデヌタ サンプリング ロゞック (ルヌプを䜿甚する堎合も含む)
      • さたざたなテヌブルからデヌタをフェッチする
      • の䜿甚 連合 О 存圚する

  3. 提案 オプション 同時実行制埡を提䟛する必芁がある堎合に非垞に圹立ちたす オプション(MAXDOP N)、ク゚リ実行プランの順序。䟋えば
    • ク゚リプランの匷制再䜜成を指定できたす。 オプション (再コンパむル)
    • ク゚リで指定された結合順序をク゚リ プランで匷制的に䜿甚するかどうかを指定できたす。 オプション匷制呜什

    詳现に぀いおは、 オプション 説明された ここで.

  4. 最も狭く、最も必芁なデヌタ スラむスを䜿甚したす。
    (むンデックス付きビュヌの堎合のように) 倧きなデヌタ セットをキャッシュに保存する必芁はなく、キャッシュからパラメヌタでデヌタをフィルタリングする必芁がありたす。
    たずえば、フィルタヌが適甚されるテヌブルがありたす。 WHERE XNUMX぀のフィヌルドが䜿甚されたす (a、b、c).

    埓来、すべおのリク゚ストには䞀定の条件がありたした a = 0 および b = 0.

    ただし、フィヌルドぞのリク゚ストは、 c より倉化しやすい。

    条件を付けたしょう a = 0 および b = 0 これは、必芁な結果セットを数千のレコヌドに制限するのに非垞に圹立ちたすが、 с 遞択を XNUMX 件のレコヌドに絞り蟌みたす。

    ここではテヌブル関数の方が良いかもしれたせん。

    たた、テヌブル 関数は実行時間の予枬可胜性が高く、䞀貫性がありたす。

䟋

䟋ずしお質問デヌタベヌスを䜿甚した実装䟋を芋おみたしょう。

リク゚ストがありたす SELECT、耇数のテヌブルを結合し、XNUMX ぀のビュヌ (OperativeQuestions) を䜿甚したす。所属は電子メヌル (経由) でチェックされたす。 存圚する)「操䜜䞊の質問」:

芁望No.1

(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM [dbo].[Questions] AS [Extent1]
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id],
[Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId],
[Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]) AND ( EXISTS (SELECT
1 AS [C1]
FROM [dbo].[OperativeQuestions] AS [Extent5]
WHERE (([Extent5].[Email] = @p__linq__0) OR (([Extent5].[Email] IS NULL) 
AND (@p__linq__0 IS NULL))) AND ([Extent5].[Id] = [Extent1].[Id])
));

ビュヌはかなり耇雑な構造をしおいたす。サブク゚リ結合があり、䞊べ替えが䜿甚されたす。 DISTINCTこれは䞀般に、かなりリ゜ヌスを倧量に消費する操䜜です。

OperativeQuestions のサンプルは玄 XNUMX 䞇件のレコヌドです。

このク゚リの䞻な問題は、倖郚ク゚リからのレコヌドに察しお、内郚サブク゚リが [OperativeQuestions] ビュヌで実行されるこずです。これにより、[Email] = @p__linq__0 の堎合、出力遞択を制限できるようになりたす (経由) 存圚する) 最倧数癟のレコヌド。

たた、サブク゚リでは [Email] = @p__linq__0 によっおレコヌドを XNUMX 回蚈算し、その埌、これらの数癟のレコヌドを ID ず質問によっお接続する必芁があり、ク゚リは高速になるように思われるかもしれたせん。

実際、すべおのテヌブルには順次接続があり、ID 質問ず OperativeQuestions からの ID の察応を確認し、電子メヌルでフィルタリングしたす。

実際、このリク゚ストは䜕䞇もの OperativeQuestions レコヌドすべおを凊理したすが、電子メヌル経由で必芁なのは関心のあるデヌタのみです。

OperativeQuestions ビュヌのテキスト:

芁望No.2

 
CREATE VIEW [dbo].[OperativeQuestions]
AS
SELECT DISTINCT Q.Id, USR.email AS Email
FROM            [dbo].Questions AS Q INNER JOIN
                         [dbo].ProcessUserAccesses AS BPU ON BPU.ProcessId = CQ.Process_Id 
OUTER APPLY
                     (SELECT   1 AS HasNoObjects
                      WHERE   NOT EXISTS
                                    (SELECT   1
                                     FROM     [dbo].ObjectUserAccesses AS BOU
                                     WHERE   BOU.ProcessUserAccessId = BPU.[Id] AND BOU.[To] IS NULL)
) AS BO INNER JOIN
                         [dbo].Users AS USR ON USR.Id = BPU.UserId
WHERE        CQ.[Exp] = 0 AND CQ.AnswerId IS NULL AND BPU.[To] IS NULL 
AND (BO.HasNoObjects = 1 OR
              EXISTS (SELECT   1
                           FROM   [dbo].ObjectUserAccesses AS BOU INNER JOIN
                                      [dbo].ObjectQuestions AS QBO 
                                                  ON QBO.[Object_Id] =BOU.ObjectId
                               WHERE  BOU.ProcessUserAccessId = BPU.Id 
                               AND BOU.[To] IS NULL AND QBO.Question_Id = CQ.Id));

DbContext の初期ビュヌ マッピング (EF Core 2)

public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}

初期の LINQ ク゚リ

var businessObjectsData = await context
    .OperativeQuestions
    .Where(x => x.Email == Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();

この特定のケヌスでは、むンフラストラクチャの倉曎を行わず、既補の結果 (「アクティブ ク゚リ」) を含む別のテヌブルを導入せずに、この問題の解決策を怜蚎しおいたす。テヌブルにデヌタを入力しお最新の状態に保぀ためのメカニズムが必芁になりたす。 。

これは良い解決策ですが、この問題を最適化する別のオプションがありたす。

䞻な目的は、OperativeQuestions ビュヌから [Email] = @p__linq__0 による゚ントリをキャッシュするこずです。

テヌブル関数 [dbo].[OperativeQuestionsUserMail] をデヌタベヌスに導入したす。

電子メヌルを入力パラメヌタヌずしお送信するず、倀のテヌブルが返されたす。

芁望No.3


CREATE FUNCTION [dbo].[OperativeQuestionsUserMail]
(
    @Email  nvarchar(4000)
)
RETURNS
@tbl TABLE
(
    [Id]           uniqueidentifier,
    [Email]      nvarchar(4000)
)
AS
BEGIN
        INSERT INTO @tbl ([Id], [Email])
        SELECT Id, @Email
        FROM [OperativeQuestions]  AS [x] WHERE [x].[Email] = @Email;
     
    RETURN;
END

これにより、事前定矩されたデヌタ構造を持぀倀のテヌブルが返されたす。

OperativeQuestionsUserMail ぞのク゚リを最適化し、最適なク゚リ プランを䜜成するには、厳密な構造が必芁です。 テヌブルをリタヌンずしお返す...

この堎合、必芁なク゚リ 1 はク゚リ 4 に倉換されたす。

芁望No.4

(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM (
    SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] (@p__linq__0)
) AS [Extent0]
INNER JOIN [dbo].[Questions] AS [Extent1] ON([Extent0].Id=[Extent1].Id)
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id], [Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId], [Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]);

DbContext でのビュヌず関数のマッピング (EF Core 2)

public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}
 
public static class FromSqlQueries
{
    public static IQueryable<OperativeQuestion> GetByUserEmail(this DbQuery<OperativeQuestion> source, string Email)
        => source.FromSql($"SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] ({Email})");
}

最終的な LINQ ク゚リ

var businessObjectsData = await context
    .OperativeQuestions
    .GetByUserEmail(Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();

実行時間は 200  800 ミリ秒から 2  20 ミリ秒などに䜎䞋し、数十倍速くなりたした。

もっず平均的に考えるず、350 ミリ秒ではなく 8 ミリ秒になりたす。

明らかな利点から、次のようなメリットも埗られたす。

  1. 読み取り負荷の党䜓的な軜枛、
  2. ブロックされる可胜性が倧幅に枛少したす
  3. 平均ブロッキング時間を蚱容倀たで短瞮する

出力

デヌタベヌス呌び出しの最適化ず埮調敎 MS SQL スルヌ LINQ 解決できる問題です。

この仕事では、泚意力ず䞀貫性が非垞に重芁です。

プロセスの開始時:

  1. リク゚ストが機胜するデヌタ (倀、遞択されたデヌタ型) を確認する必芁がありたす。
  2. このデヌタの適切なむンデックス付けを実行する
  3. テヌブル間の結合条件の正確性をチェックする

次の最適化反埩により、次のこずが明らかになりたす。

  1. リク゚ストの基瀎を䜜成し、メむンのリク゚ストフィルタヌを定矩したす
  2. 同様のク゚リブロックを繰り返し、条件の亀差を分析する
  3. SSMS たたは他の GUI で SQLサヌバヌ それ自䜓を最適化したす SQLク゚リ (䞭間デヌタ ストレヌゞを割り圓お、このストレヌゞを䜿甚しお結果のク゚リを構築したす (耇数ある堎合がありたす))
  4. 最終段階では、その結果を基瀎ずしお、 SQLク゚リ、構造が再構築されおいたす LINQク゚リ

結果ずしお LINQク゚リ 特定された最適なものず構造が同䞀になるはずです SQLク゚リ ポむント3から。

感謝

同僚に感謝したす ゞョブゞェムズ О アレックス・オズル 䌚瀟から 硬音 この資料の䜜成にご協力ください。

出所 habr.com

コメントを远加したす