データベースの問題を修正する
データベースは、Web アプリケーションにおけるパフォーマンス問題の主な原因の一つです。 データベースは膨大な量のデータを処理する必要があるため、クエリ作成時のわずかなアルゴリズムエラーでさえ深刻な結果につながる可能性があります。 これは特に ORM システムを使用する場合に当てはまります(本章の例ではエンティティフレームワークを使用しています)。
モニタリング では、 長いクエリ実行時間、 多数のデータベース接続、 多数の同じデータベースコマンド、 応答内のレコード数が多すぎるに関連するデータベースの問題が検出されます。
この章では、このような問題につながる可能性のあるコード設計の例と、それを修正する方法に関するヒントを紹介します。
遅い DB コマンド
コマンド実行時間が指定されたしきい値を超えると、 モニタリング はコマンドを実行するコードに 遅い DB コマンド の問題としてマークを付けます。 特定のコマンドの実行時間が遅い原因を 1 つだけ特定することは困難です。 このような問題は、複雑なクエリやネットワーク接続の問題などに関連している可能性があります。 コマンド実行時間が長いことが全く問題ではなく、単にアプリケーションの動作特性であり、状況を改善するための手段がない可能性も十分にあります。
デフォルトのしきい値は 500 ミリ秒です。
修正方法
問題の背後にはさまざまな理由が考えられるため、次のような唯一のアドバイスがあります。
モニタリング ウィンドウの 未解決の問題の詳細。
問題の詳細で、SQL クエリを確認します。
それでも問題の原因が明確でない場合は、データベースサーバーでクエリを直接実行してみて、通信の問題、キャッシュサイズの制限、インデックスが作成されていない列、テーブルロック、デッドロックなど、考えられるすべての原因を 1 つずつ除外してください。
過剰な DB 接続
データベースへの同時接続数が閾値を超えると、 モニタリング は接続を開いたコードを 過剰な DB 接続 の問題としてマークします。 例: 実行中に、ある関数が 100 接続、200 接続、150 接続の順に開いて閉じるとします。 閾値が 50 接続に設定されているとすると、結果として「200 接続」という問題が報告されます。
デフォルトのしきい値は 50 接続です。
以下に、接続リークの考えられる理由のいくつかを示します。
「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
}
}
コマンドの実行後に接続を閉じますが、コマンドが例外をスローした場合、接続は開いたままになります。 さらに悪いことに、接続が開いていることさえ知らずに例外を処理してしまいます。
修正方法
いずれの場合も、 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 インスタンスは、コマンドの実行後に接続を閉じません。 リーダーが何度も作成されると、対応する数の開いている接続が生成されます。
修正方法
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 コマンド
コマンド実行回数が閾値を超えると、 モニタリング は同じコマンドを複数回実行するコードを 過剰な DB コマンド 問題としてマークします。 デフォルトのしきい値は 100 コマンドです。
このチェックが存在する主な理由は、よく知られている 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 が投稿の総数(すべてのブログを選択+各ブログから投稿を選択)の場合、N+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 結果セット
データベースコマンドがしきい値を超える件数のレコードを返した場合、 モニタリング はコマンドを実行するコードに 大きい DB 結果セット の問題としてマークを付けます。 場合によっては、多くのレコードを取得することが設計上想定されています。 しかし最適ではないコーディングパターンにより、意図せず発生することもあります。
デフォルトのしきい値は 1000 レコードです。
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 データベースクエリで実行できます。 実際には、 CustomFilter は IEnumerable コレクションのみを受け入れるため、クエリは 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
次に、メモリ内の投稿コレクションにフィルターを適用します。 モニタリング の問題は、このクエリによって取得されるレコード数を示しています。
示されている例は非常に明白です。 実際のアプリケーションでは、このようなキャストは多数の呼び出し内に隠されている場合があります (たとえば、クエリフィルターチェーン内)。
修正方法
フィルター関数は明示的に 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);
}
CustomFilter は IQueryable と連動するようになったため、 CustomFilter(dbContext.Posts.Where(_ => _.PostId > 0)).Count() クエリは次のように変換されます。
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE ([p].[PostId] > 0) AND (([p].[PostId] % 2) = 0)
すべてのフィルタリングはサーバー側で行われ、アプリケーションはカウント結果を含む 1 つのレコードのみを受け取ります。
2026 年 6 月 12 日