ReSharper 2025.3 ヘルプ

データベースの問題を修正する

データベースは、Web アプリケーションのパフォーマンスの問題の主な原因の 1 つです。データベースは膨大な量のデータを処理する必要があるため、クエリを準備する際の小さなアルゴリズムエラーでさえ、重大な結果につながる可能性があります。これは、ORM システムを使用する場合に特に当てはまります (この章の例ではエンティティフレームワークを使用します)。

DPA は、長いクエリ実行時間多数のデータベース接続多数の同じデータベースコマンド応答内の多数のレコードに関連するデータベースの問題を検出します。問題は、動的プログラム分析ウィンドウのデータベースタブにグループ化されています。

DPA database issues

この章では、このような問題につながる可能性のあるコード設計の例と、それを修正する方法に関するヒントを紹介します。

DB コマンド時間

コマンドの実行時間が指定されたしきい値を超えると、DPA はコマンドを実行するコードに DB コマンド時間の問題をマークします。特定のコマンドの実行時間が長い理由を 1 つだけ特定するのは困難です。このような問題は、複雑な結果のクエリ、ネットワーク接続の問題などに関連している可能性があります。長いコマンド時間がまったく問題にならない可能性は十分にあります。これはアプリケーションの動作方法であり、状況を修正するために何もできません。

デフォルトのしきい値は 500 ミリ秒です。

直す方法

問題の背後にはさまざまな理由が考えられるため、次のような唯一のアドバイスがあります。

  1. 問題のあるコードを見つけて、動的プログラム分析ウィンドウで対応する問題を開きます。

    DPA go to issue
  2. 問題の詳細で、SQL クエリを確認します。

    DPA get SQL query
  3. それでも問題の原因が明確でない場合は、データベースサーバーでクエリを直接実行してみて、通信の問題、キャッシュサイズの制限、インデックスが作成されていない列、テーブルロック、デッドロックなど、考えられるすべての原因を 1 つずつ除外してください。

DB 接続

データベースへの同時接続数がしきい値を超えると、DPA は接続を開くコードに DB 接続問題のマークを付けます。例: 実行中、関数は 100 接続、次に 200 接続、次に 150 接続を開閉します。しきい値が 50 接続に設定されていると仮定すると、結果の問題値は「200 接続」になります。

デフォルトのしきい値は 10 接続です。

以下に、接続リークの考えられる理由のいくつかを示します。

「try」ブロックでの接続リーク

次のコードを考えてみましょう:

private static void TryFinallyLeak() { var connection = new SqlConnection("Server=DBSRV;Database=SampleDB"); try { connection.Open(); var command = new SqlCommand("select count (*) from Blogs", connection); command.ExecuteScalar(); // in case of exception here, connection won't be closed connection.Close(); } catch(Exception e) { // handle exception } }

コマンドの実行後に接続を閉じますが、コマンドが例外をスローした場合、接続は開いたままになります。さらに悪いことに、接続が開いていることさえ知らずに例外を処理してしまいます。

DPA connection leak

直す方法

いずれの場合も、finally ブロックを使用して接続を閉じる必要があります。

private static void TryFinallyLeak() { var connection = new SqlConnection("Server=DBSRV;Database=SampleDB"); try { connection.Open(); var command = new SqlCommand("select count (*) from Blogs", connection); command.ExecuteScalar(); } catch(Exception e) { // handle exception } finally { connection.Close(); // connection is closed in any case } }

SQLDataReader による接続リーク

データベースから行のストリームを読み取るために SQLDataReader を使用する場合は、正しい CommandBehavior を使用していることを確認してください。例を考えてみましょう:

private static void ReaderLeak() { var reader = GetReader(); while (reader.Read()) ; reader.Close(); // closing the reader doesn't close the connection } private static SqlDataReader GetReader() { var connection = new SqlConnection("Server=DBSRV;Database=SampleDB"); connection.Open(); var command = new SqlCommand("select * from Blogs", connection); // the created reader doesn't close the connection as // it doesn't use CommandBehavior.CloseConnection return command.ExecuteReader(); }

SqlDataReader インスタンスは、コマンドの実行後に接続を閉じません。リーダーが何度も作成されると、対応する数の開いている接続が生成されます。

DPA connection leak reader

直す方法

CommandBehavior.CloseConnection 経由で接続を閉じる SqlDataReader のインスタンスを使用していることを確認してください。ここでの問題は、デフォルトまたは別の動作の SqlDataReader が他のコードで必要になる可能性があることです。この場合、コードをリファクタリングして、必要なすべてのユースケースの動作を備えた SqlDataReader のインスタンスを作成する必要があります。

private static void ReaderLeak() { var reader = GetReader(); while (reader.Read()) ; reader.Close(); } private static SqlDataReader GetReader() { var connection = new SqlConnection("Server=DBSRV;Database=SampleDB"); connection.Open(); var command = new SqlCommand("select * from Blogs", connection); // add behavior that closes connection return command.ExecuteReader(CommandBehavior.CloseConnection); }

DB コマンド

コマンド実行回数がしきい値を超えると、DPA は同じコマンドを複数回実行するコードを DB コマンド問題としてマークします。デフォルトのしきい値は 50 コマンドです。

このチェックが存在する主な理由は、よく知られている N+1 問題を防ぐためです。例: ブログのテーブルがあり、各 Blog には多数の投稿があります。Blog から Post は 1 対多の関係です。すべてのブログのすべての投稿のリストを取得するとします。エンティティフレームワークでこれを行う簡単な方法は次のとおりです。

private void nPlus1(int count) { using var dbContext = new BlogContext(); var blogs = dbContext.Blogs.ToList(); // get list of blogs (1 query) // for each blog get all posts (N queries) foreach (var blog in blogs) { Console.WriteLine($"Posts in {blog}:"); foreach (var post in blog.Posts) Console.WriteLine($"{post}"); } } public class BlogContext: DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } // ... }

上記のコードでは、N+1 クエリが生成されます。ここで、N は投稿の総数です (すべてのブログを選択し、各ブログから投稿を選択します)。

DPA commands n plus 1

直す方法

データベースへの 1 回の要求で必要なすべてのデータを取得してみてください。例:

private void nPlus1(int count) { using var dbContext = new BlogContext(); var blogs = dbContext.Blogs .Include(b => b.Posts) // get all posts to memory (1 query) .ToList(); // the code below works locally (0 queries) foreach (var blog in blogs) { Console.WriteLine($"Posts in {blog}:"); foreach (var post in blog.Posts) Console.WriteLine($"{post}"); } }

DB レコード

データベースコマンドがしきい値を超えるレコード数を返す場合、DPA はコマンドを実行するコードに DB 接続の問題としてマークします。場合によっては、多数のレコードを取得することが設計上暗黙的に行われます。ただし、最適でないコードパターンが原因で、これが偶然に発生することもあります。

デフォルトのしきい値は 100 レコードです。

IQueryable を IEnumerable にキャストする

IQueryable は外部データソースへのクエリを意味しますが、IEnumerable はインメモリデータのみをクエリします。IEnumerable コレクションに対してクエリを実行すると、アプリケーションはまずデータベースからすべての関連データを取得し、次にメモリ内データにクエリを適用します。IQueryable を使用すると、アプリケーションはクエリを外部データベースに直接送信します。次の例を考えてみましょう。

// some custom filter that takes IEnumerable as input private IEnumerable<Post> CustomFilter(IEnumerable<Post> posts) => posts.Where(_ => _.PostId % 2 == 0); private void FilterFail() { using var dbContext = new BlogContext(); // get count of posts matching the custom filter var postCount = CustomFilter(dbContext.Posts.Where(_ => _.PostId > 0)).Count(); Console.WriteLine(postCount); } public class BlogContext: DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } // ... }

上記の例で取得したいのは、何らかの条件に一致する投稿の数だけです。理論的には、これは SELECT COUNT データベースクエリで実行できます。実際には、CustomFilterIEnumerable コレクションのみを受け入れるため、クエリは IQueryable から IEnumerable にキャストされます。その結果、行 CustomFilter(dbContext.Posts.Where(_ => _.PostId > 0)).Count() はすべての投稿を次のようにメモリにロードします。

SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Title] FROM [Posts] AS [p] WHERE [p].[PostId] > 0

次に、メモリ内の投稿のコレクションにフィルターが適用されます。DPA の問題は、このクエリが原因で受け取ったレコードの数を示しています。

DB records IEnumerable

示されている例は非常に明白です。実際のアプリケーションでは、このようなキャストは多数の呼び出し内に隠されている場合があります (たとえば、クエリフィルターチェーン内)。

直す方法

フィルター関数は明示的に IQueryable コレクションを使用する必要があります。例:

private IQueryable<Post> CustomFilter(IQueryable<Post> posts) => posts.Where(_ => _.PostId % 2 == 0); private void FilterFail() { using var dbContext = new BlogContext(); // get count of posts matching the custom filter var postCount = CustomFilter(dbContext.Posts.Where(_ => _.PostId > 0)).Count(); Console.WriteLine(postCount); }

CustomFilterIQueryable と連動するようになったため、CustomFilter(dbContext.Posts.Where(_ => _.PostId > 0)).Count() クエリは次のように変換されます。

SELECT COUNT(*) FROM [Posts] AS [p] WHERE ([p].[PostId] > 0) AND (([p].[PostId] % 2) = 0)

すべてのフィルタリングはサーバー側で行われ、アプリケーションはカウント結果を含む 1 つのレコードのみを受け取ります。

2024 年 9 月 23 日