使用 C# 和 EF Core 进行快速 SQL 批量插入

无论您是构建数据分析平台、迁移遗留系统还是引入大量新用户,都可能会需要在数据库中插入大量数据。

一张一张地放入唱片的感觉就像看着油漆慢慢变干一样。传统的方法行不通。

因此,了解使用 C# 和 EF Core 的快速批量插入技术变得至关重要。

在今天的问题中,我们将探讨在 C# 中执行批量插入的几种选项:

  • Dapper
  • EF 核心
  • EF Core 批量扩展
  • SQL 批量复制
这些示例基于SQL Server中User具有相应表的类。Users

public class User
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string PhoneNumber { get; set; }
}

这不是批量插入实现的完整列表。还有一些选项我没有探索,例如手动生成 SQL 语句和使用表值参数。

EF Core 简单方法
让我们从使用 EF Core 的简单示例开始。我们创建一个ApplicationDbContext实例,添加一个User对象,然后调用SaveChangesAsync。这会将每条记录逐一插入数据库。换句话说,每条记录都需要往返数据库一次。

using var context = new ApplicationDbContext();

foreach (var user in GetUsers())
{
    context.Users.Add(user);

    await context.SaveChangesAsync();
}

结果正如你想象的那么糟糕:

EF Core - Add one and save, for 100 users: 20 ms
EF Core - Add one and save, for 1,000 users: 260 ms
EF Core - Add one and save, for 10,000 users: 8,860 ms

100,000我省略了带有和记录的结果,1,000,000因为它们执行时间太长。

我们将以此作为“如何不进行批量插入”的示例。

Dapper 简单插入
Dapper是一个简单的 .NET SQL 到对象映射器。它允许我们轻松地将对象集合插入数据库。

我正在使用 Dapper 的功能将集合解开为 SQLINSERT语句。

using var connection = new SqlConnection(connectionString);
connection.Open();

const string sql =
    @"
    INSERT INTO Users (Email, FirstName, LastName, PhoneNumber)
    VALUES (@Email, @FirstName, @LastName, @PhoneNumber);
   
";

await connection.ExecuteAsync(sql, GetUsers());

结果比最初的例子好得多:

Dapper - Insert range, for 100 users: 10 ms
Dapper - Insert range, for 1,000 users: 113 ms
Dapper - Insert range, for 10,000 users: 1,028 ms
Dapper - Insert range, for 100,000 users: 10,916 ms
Dapper - Insert range, for 1,000,000 users: 109,065 ms

EF Core 添加和保存
但是,EF Core 仍然没有放弃。第一个例子故意做得很差。EF Core 可以将多个 SQL 语句批量处理,所以让我们使用它。

如果我们进行简单的更改,就可以获得明显更好的性能。首先,我们将所有对象添加到ApplicationDbContext。然后,我们只调用SaveChangesAsync一次。

EF 将创建批处理 SQL 语句(将多个INSERT语句组合在一起)并将它们一起发送到数据库。这减少了往返数据库的次数,从而提高了性能。

using var context = new ApplicationDbContext();

foreach (var user in GetUsers())
{
    context.Users.Add(user);
}

await context.SaveChangesAsync();

以下是此实现的基准测试结果:

EF Core - Add all and save, for 100 users: 2 ms
EF Core - Add all and save, for 1,000 users: 18 ms
EF Core - Add all and save, for 10,000 users: 203 ms
EF Core - Add all and save, for 100,000 users: 2,129 ms
EF Core - Add all and save, for 1,000,000 users: 21,557 ms

请记住,Dapper插入记录花费了109 秒。我们可以在约 21 秒1,000,000内使用 EF Core 批量查询实现相同的效果。

EF Core AddRange 和 Save
这是上一个示例的替代方法。Add我们不必调用所有对象,而是可以调用AddRange并传入一个集合。

我想展示这个实现,因为我更喜欢它而不是以前的一个。

using var context = new ApplicationDbContext();

context.Users.AddRange(GetUsers());

await context.SaveChangesAsync();

结果与前面的例子非常相似:

EF Core - Add range and save, for 100 users: 2 ms
EF Core - Add range and save, for 1,000 users: 18 ms
EF Core - Add range and save, for 10,000 users: 204 ms
EF Core - Add range and save, for 100,000 users: 2,111 ms
EF Core - Add range and save, for 1,000,000 users: 21,605 ms

EF Core 批量扩展
有一个很棒的库叫做EF Core Bulk Extensions,我们可以使用它来提高性能。这个库可以做很多事情,而不仅仅是批量插入,所以我建议你去探索一下。这个库是开源的,如果你符合免费使用标准,它有一个社区许可证。查看许可部分了解更多详情。

对于我们的用例来说,该BulkInsertAsync方法是一个很好的选择。我们可以传递对象集合,它将执行 SQL 批量插入


using var context = new ApplicationDbContext();

await context.BulkInsertAsync(GetUsers());

性能同样令人惊叹:

EF Core - Bulk Extensions, for 100 users: 1.9 ms
EF Core - Bulk Extensions, for 1,000 users: 8 ms
EF Core - Bulk Extensions, for 10,000 users: 76 ms
EF Core - Bulk Extensions, for 100,000 users: 742 ms
EF Core - Bulk Extensions, for 1,000,000 users: 8,333 ms

相比之下,使用 EF Core 批量查询插入记录大约需要21 秒。使用Bulk Extensions库只需8 秒1,000,000即可完成相同操作。

SQL 批量复制
最后,如果我们无法从 EF Core 获得所需的性能,我们可以尝试使用SqlBulkCopy。SQL Server 原生支持批量复制操作,所以让我们使用它。
这个实现比 EF Core 示例稍微复杂一些。我们需要配置实例SqlBulkCopy并创建一个DataTable包含我们要插入的对象。

using var bulkCopy = new SqlBulkCopy(ConnectionString);

bulkCopy.DestinationTableName = "dbo.Users";

bulkCopy.ColumnMappings.Add(nameof(User.Email),
"Email");
bulkCopy.ColumnMappings.Add(nameof(User.FirstName),
"FirstName");
bulkCopy.ColumnMappings.Add(nameof(User.LastName),
"LastName");
bulkCopy.ColumnMappings.Add(nameof(User.PhoneNumber),
"PhoneNumber");

await bulkCopy.WriteToServerAsync(GetUsersDataTable());

然而,性能却非常快:

SQL Bulk Copy, for 100 users: 1.7 ms
SQL Bulk Copy, for 1,000 users: 7 ms
SQL Bulk Copy, for 10,000 users: 68 ms
SQL Bulk Copy, for 100,000 users: 646 ms
SQL Bulk Copy, for 1,000,000 users: 7,339 ms

下面展示了如何创建DataTable并用对象列表填充它:

DataTable GetUsersDataTable()
{
    var dataTable = new DataTable();

    dataTable.Columns.Add(nameof(User.Email), typeof(string));
    dataTable.Columns.Add(nameof(User.FirstName), typeof(string));
    dataTable.Columns.Add(nameof(User.LastName), typeof(string));
    dataTable.Columns.Add(nameof(User.PhoneNumber), typeof(string));

    foreach (var user in GetUsers())
    {
        dataTable.Rows.Add(
            user.Email, user.FirstName, user.LastName, user.PhoneNumber);
    }

    return dataTable;
}


结果
以下是所有批量插入实现的结果:

| Method             |   Size     |      Speed
|------------------- |----------- |----------------:
| EF_OneByOne        | 100        |      19.800 ms |
| EF_OneByOne        | 1000       |     259.870 ms |
| EF_OneByOne        | 10000      |   8,860.790 ms |
| EF_OneByOne        | 100000     |            N/A |
| EF_OneByOne        | 1000000    |            N/A |

| Dapper_Insert      | 100        |      10.650 ms |
| Dapper_Insert      | 1000       |     113.137 ms |
| Dapper_Insert      | 10000      |   1,027.979 ms |
| Dapper_Insert      | 100000     |  10,916.628 ms |
| Dapper_Insert      | 1000000    | 109,064.815 ms |

| EF_AddAll          | 100        |       2.064 ms |
| EF_AddAll          | 1000       |      17.906 ms |
| EF_AddAll          | 10000      |     202.975 ms |
| EF_AddAll          | 100000     |   2,129.370 ms |
| EF_AddAll          | 1000000    |  21,557.136 ms |

| EF_AddRange        | 100        |       2.035 ms |
| EF_AddRange        | 1000       |      17.857 ms |
| EF_AddRange        | 10000      |     204.029 ms |
| EF_AddRange        | 100000     |   2,111.106 ms |
| EF_AddRange        | 1000000    |  21,605.668 ms |

| BulkExtensions     | 100        |       1.922 ms |
| BulkExtensions     | 1000       |       7.943 ms |
| BulkExtensions     | 10000      |      76.406 ms |
| BulkExtensions     | 100000     |     742.325 ms |
| BulkExtensions     | 1000000    |   8,333.950 ms |

| BulkCopy           | 100        |       1.721 ms |
| BulkCopy           | 1000       |       7.380 ms |
| BulkCopy           | 10000      |      68.364 ms |
| BulkCopy           | 100000     |     646.219 ms |
| BulkCopy           | 1000000    |   7,339.298 ms |


总结
SqlBulkCopy拥有最高原始速度的王冠。然而,EF Core Bulk Extensions提供了出色的性能,同时保持了 Entity Framework Core 以易用性而闻名。

最佳选择取决于您的项目的具体需求:

  • 性能才是最重要的?SqlBulkCopy是您的解决方案。
  • 需要出色的速度和精简的开发?EF Core 是一个明智的选择。