循环插入太慢?试试 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 选项

资源和文档

相关推荐
半亩码田6 小时前
C#转Python第3.1篇:Python 的 class 没有访问修饰符?面向对象的另一条路
开发语言·python·c#
唐青枫12 小时前
别再手写 args[0]:C#.NET CommandLineParser 命令行工具实战详解
c#·.net
LccKyI13 小时前
C#学习day06(方法/函数及其关键字,附思维导图)
经验分享·学习·c#
.Peter14 小时前
.NET/WPF 程序在部分 Windows 电脑无法保存用户环境变量:使用 DPAPI 实现安全兼容
windows·信息安全·c#·.net·wpf·环境变量·dpapi
农村小镇哥14 小时前
怎么把HTM格式转化成WORD
ui·c#·word
农村小镇哥14 小时前
C#读取CSV文件的方法
服务器·数据库·c#
FuckPatience15 小时前
C# 报错参数无效
c#
北域码匠1 天前
PID 控制算法完整详解(C# 原生实现,无第三方库)
c#·pid控制·工控开发·闭环控制·自动控制算法·数字pid·增量式pid
离陌在学C#1 天前
C# 重载与重写:深入理解面向对象编程的核心概念
java·c#
asdzx671 天前
Java 实战:基于 Spire.Doc 高效提取 Word 文档文本与图片
java·c#·word