循环插入太慢?试试 C#.NET SqlBulkCopy,一次导入上百万数据

简介

SqlBulkCopy.NET 中针对 SQL Server 的高性能批量数据导入类,通过最小化网络往返和利用 SQL Server 的批量加载机制,实现远超传统 INSERT 语句的性能(通常快 10-100 倍)。它通过利用 SQL Server 的批量插入机制(BCP,Bulk Copy Protocol),显著提高了数据导入的效率,特别适合大数据量场景。

背景和作用

.NET 应用中,插入大量数据到 SQL Server 数据库时,传统的逐行插入(如通过 EF CoreAddSaveChanges)效率低下,容易导致性能瓶颈。SqlBulkCopy 解决了以下问题:

  • 高性能批量插入:通过批量操作,减少数据库往返,提升插入速度。

  • 大吞吐量支持:适合导入数千到数百万行数据。

  • 灵活的数据源:支持从 DataTable、DataReader 或其他实现 IDataReader 的数据源导入。

  • 事务支持:允许在事务中执行批量插入,确保数据一致性。

安装与配置

SqlBulkCopySQL Server 提供的一个类,位于 System.Data.SqlClient 中,所以需要确保在项目中引用了相应的包。

  • NuGet

如果项目是 .NET Core.NET 5+,可以使用 Microsoft.Data.SqlClient 包。

shell 复制代码
Install-Package Microsoft.Data.SqlClient
  • 命名空间引用
csharp 复制代码
using Microsoft.Data.SqlClient;
  • SQL Server 环境

需要 SQL Server 数据库支持 SqlBulkCopy,并且操作需要数据库连接字符串和相关表结构。

核心功能

功能 描述
SqlBulkCopy.WriteToServer 将数据从内存写入到 SQL Server 数据库
SqlBulkCopy.BulkCopyTimeout 设置执行超时。默认 30 秒,超过会抛出异常
SqlBulkCopy.BatchSize 批量插入的行数。控制一次性提交的行数,提升性能
SqlBulkCopy.DestinationTableName 指定目标数据库表名
SqlBulkCopy.ColumnMappings 设置源列到目标列的映射关系
SqlBulkCopy.NotifyAfter 设置每处理指定行数时触发 SqlRowsCopied 事件
SqlBulkCopy.SqlRowsCopied 捕获批量插入的数据统计信息(如行数)

主要 API 用法

基本用法

csharp 复制代码
using Microsoft.Data.SqlClient;

public void BulkInsert()
{
    // 连接到数据库
    string connectionString = "Your_Connection_String";
    using var connection = new SqlConnection(connectionString);
    connection.Open();

    // 使用 SqlBulkCopy 执行批量插入
    using var bulkCopy = new SqlBulkCopy(connection)
    {
        DestinationTableName = "TargetTable"  // 目标表名
    };

    // 创建一个 DataTable 或 IDataReader
    var dataTable = GetDataTable(); // 从数据库或其他数据源获取数据
    bulkCopy.WriteToServer(dataTable); // 执行插入操作

    Console.WriteLine("批量插入成功!");
}

从 IDataReader 插入数据

从另一个数据库读取数据并插入:

csharp 复制代码
using Microsoft.Data.SqlClient;

class Program
{
    static void Main()
    {
        string sourceConnString = "Server=source;Database=sourcedb;Trusted_Connection=True;";
        string destConnString = "Server=localhost;Database=testdb;Trusted_Connection=True;";

        using var sourceConn = new SqlConnection(sourceConnString);
        using var destConn = new SqlConnection(destConnString);
        sourceConn.Open();
        destConn.Open();

        using var command = new SqlCommand("SELECT Id, Name FROM SourceUsers", sourceConn);
        using var reader = command.ExecuteReader();

        using var bulkCopy = new SqlBulkCopy(destConn)
        {
            DestinationTableName = "Users",
            BatchSize = 1000
        };

        // 列映射(如果列名不匹配)
        bulkCopy.ColumnMappings.Add("Id", "UserId");
        bulkCopy.ColumnMappings.Add("Name", "UserName");

        bulkCopy.WriteToServer(reader);
        Console.WriteLine("Data copied from source to destination");
    }
}

异步插入

使用异步方法提升性能:

csharp 复制代码
using Microsoft.Data.SqlClient;
using System.Data;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string connectionString = "Server=localhost;Database=testdb;Trusted_Connection=True;";
        var dataTable = new DataTable("Users");
        dataTable.Columns.Add("Id", typeof(int));
        dataTable.Columns.Add("Name", typeof(string));

        for (int i = 1; i <= 10000; i++)
        {
            dataTable.Rows.Add(i, $"User{i}");
        }

        using var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();

        using var bulkCopy = new SqlBulkCopy(connection)
        {
            DestinationTableName = "Users",
            BatchSize = 1000
        };

        await bulkCopy.WriteToServerAsync(dataTable);
        Console.WriteLine("Inserted 10,000 rows asynchronously");
    }
}

设置映射关系

如果源数据列与目标表列名称不同,可以通过 ColumnMappings 来映射它们。

csharp 复制代码
using var bulkCopy = new SqlBulkCopy(connection)
{
    DestinationTableName = "TargetTable"
};
bulkCopy.ColumnMappings.Add("SourceColumn1", "DestinationColumn1");
bulkCopy.ColumnMappings.Add("SourceColumn2", "DestinationColumn2");

bulkCopy.WriteToServer(dataTable);

设置批量大小与超时

csharp 复制代码
using var bulkCopy = new SqlBulkCopy(connection)
{
    DestinationTableName = "TargetTable",
    BatchSize = 1000,          // 每次提交 1000 行数据
    BulkCopyTimeout = 600      // 设置超时时间为 600 秒
};
bulkCopy.WriteToServer(dataTable);

使用事件通知批量插入进度

csharp 复制代码
using var bulkCopy = new SqlBulkCopy(connection)
{
    DestinationTableName = "TargetTable",
    NotifyAfter = 1000       // 每处理 1000 行数据时触发事件
};

bulkCopy.SqlRowsCopied += (sender, e) =>
{
    Console.WriteLine($"已插入 {e.RowsCopied} 行数据");
};

bulkCopy.WriteToServer(dataTable);

事务与错误处理

csharp 复制代码
using (var transaction = connection.BeginTransaction())
{
    try
    {
        bulkCopy.BatchSize = 5000;
        bulkCopy.BulkCopyTimeout = 120;
        bulkCopy.WriteToServer(data);
        transaction.Commit();
    }
    catch (SqlException ex)
    {
        transaction.Rollback();
        // 处理错误(如违反约束)
        foreach (SqlError error in ex.Errors)
        {
            Console.WriteLine($"错误: {error.Message}");
        }
    }
}

性能优化

批量大小(BatchSize)

设置合适的批量大小(BatchSize)可以提高性能。默认情况下,每次插入 10000 行。如果批量大小过大,可能导致内存消耗过高;太小则会影响性能。一般来说,批量大小在 1000-10000 行之间最为合适。

禁用或减少日志

如果不需要插入时的日志记录,可以通过设置 SqlBulkCopyOptions 来禁用:

csharp 复制代码
using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepNulls);

禁用外键约束

批量插入时,外键约束会导致性能下降。如果可以禁用外键约束,插入会更快:

sql 复制代码
ALTER TABLE TargetTable NOCHECK CONSTRAINT ALL;

在插入后,可以重新启用约束:

sql 复制代码
ALTER TABLE TargetTable CHECK CONSTRAINT ALL;

关闭索引

插入大量数据时,可以暂时禁用索引,插入完成后再重建索引,性能将显著提高。

sql 复制代码
ALTER INDEX ALL ON TargetTable DISABLE;
-- 插入数据
ALTER INDEX ALL ON TargetTable REBUILD;

使用示例

示例 1:将 List<T> 数据批量插入到数据库

csharp 复制代码
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Department { get; set; }
}

public void BulkInsertEmployees(List<Employee> employees)
{
    using var connection = new SqlConnection("Your_Connection_String");
    connection.Open();

    // 将 List 转为 DataTable
    var dataTable = ConvertToDataTable(employees);

    using var bulkCopy = new SqlBulkCopy(connection)
    {
        DestinationTableName = "Employees",
        BatchSize = 5000,
        NotifyAfter = 1000
    };

    bulkCopy.SqlRowsCopied += (sender, e) =>
    {
        Console.WriteLine($"已插入 {e.RowsCopied} 行数据");
    };

    bulkCopy.WriteToServer(dataTable);
}

public DataTable ConvertToDataTable(List<Employee> employees)
{
    var table = new DataTable();
    table.Columns.Add("Id", typeof(int));
    table.Columns.Add("Name", typeof(string));
    table.Columns.Add("Department", typeof(string));

    foreach (var employee in employees)
    {
        table.Rows.Add(employee.Id, employee.Name, employee.Department);
    }

    return table;
}

示例 2:处理 CSV 文件批量插入

csharp 复制代码
public void BulkInsertFromCsv(string filePath)
{
    using var connection = new SqlConnection("Your_Connection_String");
    connection.Open();

    var dataTable = new DataTable();
    dataTable.Columns.Add("Id", typeof(int));
    dataTable.Columns.Add("Name", typeof(string));
    dataTable.Columns.Add("Age", typeof(int));

    var lines = File.ReadLines(filePath);
    foreach (var line in lines.Skip(1))  // 假设第一行是表头
    {
        var values = line.Split(',');
        dataTable.Rows.Add(int.Parse(values[0]), values[1], int.Parse(values[2]));
    }

    using var bulkCopy = new SqlBulkCopy(connection)
    {
        DestinationTableName = "People",
        BatchSize = 5000
    };

    bulkCopy.WriteToServer(dataTable);
}

使用场景与限制

理想应用场景

  • 数据迁移:从旧系统导入历史数据

  • ETL 处理:数据仓库定期加载

  • 实时数据流:传感器/IoT 设备批量上传

  • 报表生成:预计算数据批量存储

  • 缓存预热:初始化内存数据库

功能限制

  • 仅限 SQL Server:不直接支持其他数据库

  • 无更新能力:仅插入新数据(需配合临时表实现更新)

  • 触发器影响:默认触发插入触发器(可通过选项控制)

  • 标识列处理:需显式设置 KeepIdentity 选项

资源和文档

相关推荐
hhh3u3u3u13 小时前
Visual C++ 6.0中文版安装包下载教程及win11安装教程
java·c语言·开发语言·c++·python·c#·vc-1
加号313 小时前
【C#】实现沃德普线光控制器通信控制(附完整源码)
开发语言·c#
lzhdim14 小时前
SharpCompress:跨平台的 C# 压缩与解压库
开发语言·c#
~plus~16 小时前
.NET 8 C# 委托与事件实战教程
网络·c#·.net·.net 8·委托与事件·c#进阶
beyond谚语17 小时前
接口&抽象类
c#·接口隔离原则·抽象类
新手小新17 小时前
C#学习笔记1-在VS CODE部署C#开发环境
笔记·学习·c#
rockey62720 小时前
AScript动态脚本多语言环境支持
sql·c#·.net·script·eval·function·动态脚本
dotNET实验室20 小时前
ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑
.net
ou.cs21 小时前
c# SemaphoreSlim保姆级教程
开发语言·网络·c#
龙侠九重天21 小时前
ML.NET 实战:快速构建分类模型
分类·数据挖掘·c#·.net