JetBrains Rider 2025.2 Help

修复数据库问题

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

DPA 检测与 查询执行时间过长数据库连接数量过多相同数据库命令数量过多以及 响应中记录数量过多相关的数据库问题。 这些问题被归类在 数据库 选项卡中的 Dynamic Program Analysis 窗口中。

DPA 数据库问题

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

数据库命令时间

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

默认阈值是 500 毫秒。

修正方法

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

  1. 找到有问题的代码,并在 Dynamic Program Analysis 窗口中打开相应的问题。

    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 实例在执行命令后不会关闭连接。 如果多次创建读取器,这将产生相应数量的打开连接。

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 问题。 例如,您有一个博客表,每个 博客 中都有一些帖子。 因此, 博客Post 是一个一对多的关系。 假设您想获取所有博客中所有帖子的列表。 使用 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月 26日