ReSharper 2025.2 Help

修复数据库问题

数据库是 Web 应用程序中性能问题的主要来源之一。 由于数据库需要处理大量数据,即使在准备查询时出现轻微的算法错误,也会导致严重的后果。 这尤其适用于使用 ORM 系统的情况(本章示例使用 Entity Framework)。

DPA 检测与 长查询执行时间大量数据库连接大量相同数据库命令以及 响应中的大量记录相关的数据库问题。 这些问题会分组显示在 数据库 选项卡中的 动态程序分析 窗口中。

DPA 数据库问题

在本章中,您将找到可能导致此类问题的代码设计示例以及如何修复它的提示。

数据库命令时间

如果命令执行时间超过指定的阈值,DPA 会将运行该命令的代码标记为 DB 命令时间 问题。 很难单独找出特定命令执行时间过长的原因。 此类问题可能与复杂的结果查询、网络连接问题等相关。 很有可能长时间的命令执行根本不是问题——这可能只是应用程序的正常工作方式,无法解决该情况。

默认阈值为 500 毫秒。

修正方法

由于导致此问题的原因可能有多种,因此唯一可以给予的建议是:

  1. 找到有问题的代码并打开 动态程序分析 窗口中的对应问题。

    DPA 转到问题
  2. 在问题详情中检查 SQL 查询。

    DPA 获取 SQL 查询
  3. 如果仍然不清楚问题原因,可以直接在数据库服务器上运行查询,并逐一排除所有可能的原因,例如通信问题、缓存大小限制、未索引的列、表锁、死锁等。

数据库连接

如果数据库的同时打开连接数超过阈值,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 连接泄漏

修正方法

您必须使用 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 实例在执行命令后不会关闭连接。 如果多次创建 reader,这会导致相应数量的连接保持打开。

DPA 连接泄漏读取器

修正方法

请确保您使用的 SqlDataReader 实例通过 CommandBehavior.CloseConnection 关闭连接。 此处的问题是,某些其他代码可能需要默认或其他行为的 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); }

数据库命令

如果命令执行次数超过阈值,DPA 会将多次运行相同命令的代码标记为 DB 命令 问题。 默认阈值为 50 个命令。

此检查存在的主要原因是为了防止众所周知的 N+1 问题。 例如,您有一个博客表,每个 Blog 中包含多个帖子。 因此, BlogPost 是一个一对多的关系。 假设您想获取所有博客中所有帖子的列表。 使用 Entity Framework 执行此操作的直接方法是:

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 命令 n 加 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}"); } }

数据库记录

如果数据库命令返回的记录数超过阈值,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 数据库查询完成。 实际上,由于 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

接下来,过滤器将应用于内存中的帖子集合。 DPA 问题显示了由于此查询我们接收到的记录数量:

数据库记录 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)

因此,所有过滤操作都发生在服务器端,应用程序只接收到一个包含计数结果的记录。

最后修改日期: 2025年 9月 27日