Entity Framework是.NET平台下的一种简单易用的ORM框架,它既便于Domain Model和持久层的OO设计,也提高了代码的可维护性。但在使用中发现,有几类业务场景是EF不太擅长的,比如批量写入大量同类数据,为此本人做了一些对比测试,以供大家参考。

现假设我们需要做一个用户批量导入的功能,需要从某处导入1k~1w个User到SQLServer数据库,本人听说过的常见做法有如下几种:

  1. 使用ADO.NET单条SqlCommand执行1w次(根据常识作为EF的替代其性能还不够格,所以就不做测试了)
  2. 使用StringBuilder拼接SQL语句,将1w条Insert语句拼接成1到若干条SqlCommand执行
  3. 使用EntityFramework的基本功能进行插入
  4. 使用SqlBulkCopy进行批量插入
  5. 使用存储过程,其中的2种分支分别对应上述1、2用例,另外还有1种表参数存储过程。

数据库准备工作:

 CREATE DATABASE BulkInsertTest
GO USE BulkInsertTest
GO CREATE TABLE [dbo].[User](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[Birthday] [date] NOT NULL,
[Gender] [char](1) NOT NULL,
[Email] [nvarchar](50) NOT NULL,
[Deleted] [bit] NOT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] GO CREATE PROCEDURE [dbo].[InsertUser]
@Name nvarchar(50)
,@Birthday date
,@Gender char(1)
,@Email nvarchar(50)
,@Deleted bit
AS
BEGIN
INSERT INTO [BulkInsertTest].[dbo].[User]
([Name]
,[Birthday]
,[Gender]
,[Email]
,[Deleted])
VALUES
(@Name,@Birthday,@Gender,@Email,@Deleted) END /* Create a table type. */
CREATE TYPE LocationTableType AS TABLE
( Name nvarchar(50)
,Birthday date
,Gender char(1)
,Email nvarchar(50)
,Deleted bit );
GO /* Create a procedure to receive data for the table-valued parameter. */
CREATE PROCEDURE [dbo].[InsertUsers]
@Users LocationTableType
AS
SET NOCOUNT ON
INSERT INTO [dbo].[User]
([Name]
,[Birthday]
,[Gender]
,[Email]
,[Deleted])
SELECT *
FROM @Users; GO

创建DbContext和User Entity的C#代码:

 using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity; namespace ConsoleApplication5
{
public class MyDbContext : DbContext
{
public MyDbContext() : base("MyDbContext") { } public MyDbContext(string connectionString) :
base(connectionString)
{ } public DbSet<User> Users { get; set; }
} [Table("User")]
public class User
{
[Key]
public int Id { get; set; } public string Name { get; set; } public DateTime Birthday { get; set; } public string Gender { get; set; } public string Email { get; set; } public bool Deleted { get; set; }
}
}

测试程序C#代码:

 using System;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Text; namespace ConsoleApplication5
{
class Program
{
private const string ConnectionString = "Data Source=.;Initial Catalog=BulkInsertTest;User=sa;Password=IGTtest1";
private const int Times = ;
private const int Entries = ; static void Main(string[] args)
{
long sumBulkCopyTime = , sumSqlCmdsTime = , sumMultiSpTime = , sumTableSpTime = , sumEfTime = ;
long maxBulkCopyTime = , maxSqlCmdsTime = , maxMultiSpTime = , maxTableSpTime = , maxEfTime = ;
for (int i = ; i < Times; i++)
{
long bulkCopyTime = InsertBySqlBulkCopy();
sumBulkCopyTime += bulkCopyTime;
maxBulkCopyTime = Math.Max(maxBulkCopyTime, bulkCopyTime); long sqlCmdsTime = InsertBySqlCmds();
sumSqlCmdsTime += sqlCmdsTime;
maxSqlCmdsTime = Math.Max(maxSqlCmdsTime, sqlCmdsTime); long multiSpTime = InsertByMultiStoreProcedure();
sumMultiSpTime += multiSpTime;
maxMultiSpTime = Math.Max(maxMultiSpTime, multiSpTime); long tableSpTime = InsertByTableStoreProcedure();
sumTableSpTime += tableSpTime;
maxTableSpTime = Math.Max(maxTableSpTime, tableSpTime); long efTime = InsertByEntityFramework();
sumEfTime += efTime;
maxEfTime = Math.Max(maxEfTime, efTime);
}
Console.WriteLine(new string('-', ));
Console.WriteLine("Time Cost of SqlBulkCopy: avg:{0}ms, max:{1}ms", sumBulkCopyTime / Times, maxBulkCopyTime);
Console.WriteLine("Time Cost of SqlCommands: avg:{0}ms, max:{1}ms", sumSqlCmdsTime / Times, maxSqlCmdsTime);
Console.WriteLine("Time Cost of MultiStoreProcedure: avg:{0}ms, max:{1}ms", sumMultiSpTime / Times, maxMultiSpTime);
Console.WriteLine("Time Cost of TableStoreProcedure: avg:{0}ms, max:{1}ms", sumTableSpTime / Times, maxTableSpTime);
Console.WriteLine("Time Cost of EntityFramework: avg:{0}ms, max:{1}ms", sumEfTime / Times, maxEfTime);
Console.ReadLine();
} private static long InsertBySqlCmds()
{
Stopwatch stopwatch = Stopwatch.StartNew();
using (var connection = new SqlConnection(ConnectionString))
{
SqlTransaction transaction = null;
connection.Open();
try
{
transaction = connection.BeginTransaction();
StringBuilder sb = new StringBuilder();
for (int j = ; j < Entries; j++)
{
sb.AppendFormat(@"INSERT INTO dbo.[User] ([Name],[Birthday],[Gender],[Email],[Deleted])
VALUES('{0}','{1:yyyy-MM-dd}','{2}','{3}',{4});", "name" + j, DateTime.Now.AddDays(j), 'M', "user" + j + "@abc.com", );
}
var sqlCmd = connection.CreateCommand();
sqlCmd.CommandText = sb.ToString();
sqlCmd.Transaction = transaction;
sqlCmd.ExecuteNonQuery();
transaction.Commit();
}
catch
{
if (transaction != null)
{
transaction.Rollback();
}
throw;
}
}
stopwatch.Stop();
Console.WriteLine("SqlCommand time cost: {0}ms", stopwatch.ElapsedMilliseconds);
return stopwatch.ElapsedMilliseconds;
} private static long InsertByMultiStoreProcedure()
{
Stopwatch stopwatch = Stopwatch.StartNew();
using (var connection = new SqlConnection(ConnectionString))
{
SqlTransaction transaction = null;
connection.Open();
for (int i = ; i < ; i++)
{
try
{
transaction = connection.BeginTransaction();
StringBuilder sb = new StringBuilder();
for (int j = ; j < Entries/; j++)
{
sb.AppendFormat(@"EXECUTE [dbo].[InsertUser] '{0}','{1:yyyy-MM-dd}','{2}','{3}',{4};",
"name" + j, DateTime.Now.AddDays(j), 'M', "user" + j + "@abc.com", );
}
var sqlCmd = connection.CreateCommand();
sqlCmd.CommandText = sb.ToString();
sqlCmd.Transaction = transaction;
sqlCmd.ExecuteNonQuery();
transaction.Commit();
}
catch
{
if (transaction != null)
{
transaction.Rollback();
}
throw;
}
}
}
stopwatch.Stop();
Console.WriteLine("MultiStoreProcedure time cost: {0}ms", stopwatch.ElapsedMilliseconds);
return stopwatch.ElapsedMilliseconds;
} private static long InsertByTableStoreProcedure()
{
Stopwatch stopwatch = Stopwatch.StartNew();
var table = PrepareDataTable();
using (var connection = new SqlConnection(ConnectionString))
{
SqlTransaction transaction = null;
connection.Open();
try
{
transaction = connection.BeginTransaction();
var sqlCmd = connection.CreateCommand();
sqlCmd.CommandText = "InsertUsers";
sqlCmd.CommandType = CommandType.StoredProcedure;
sqlCmd.Parameters.Add(new SqlParameter("@Users", SqlDbType.Structured));
sqlCmd.Parameters["@Users"].Value = table;
sqlCmd.Transaction = transaction;
sqlCmd.ExecuteNonQuery();
transaction.Commit();
}
catch
{
if (transaction != null)
{
transaction.Rollback();
}
throw;
}
}
stopwatch.Stop();
Console.WriteLine("TableStoreProcedure time cost: {0}ms", stopwatch.ElapsedMilliseconds);
return stopwatch.ElapsedMilliseconds;
} private static long InsertBySqlBulkCopy()
{
Stopwatch stopwatch = Stopwatch.StartNew(); var table = PrepareDataTable();
SqlBulkCopy(table); stopwatch.Stop();
Console.WriteLine("SqlBulkCopy time cost: {0}ms", stopwatch.ElapsedMilliseconds);
return stopwatch.ElapsedMilliseconds;
} private static DataTable PrepareDataTable()
{
DataTable table = new DataTable();
table.Columns.Add("Name", typeof (string));
table.Columns.Add("Birthday", typeof (DateTime));
table.Columns.Add("Gender", typeof (char));
table.Columns.Add("Email", typeof (string));
table.Columns.Add("Deleted", typeof (bool));
for (int i = ; i < Entries; i++)
{
var row = table.NewRow();
row["Name"] = "name" + i;
row["Birthday"] = DateTime.Now.AddDays(i);
row["Gender"] = 'M';
row["Email"] = "user" + i + "@abc.com";
row["Deleted"] = false;
table.Rows.Add(row);
}
return table;
} private static void SqlBulkCopy(DataTable dataTable)
{
using (var connection = new SqlConnection(ConnectionString))
{
SqlTransaction transaction = null;
connection.Open();
try
{
transaction = connection.BeginTransaction();
using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction))
{
sqlBulkCopy.BatchSize = dataTable.Rows.Count; sqlBulkCopy.DestinationTableName = "[User]";
//sqlBulkCopy.ColumnMappings.Add("Id", "Id");
sqlBulkCopy.ColumnMappings.Add("Name", "Name");
sqlBulkCopy.ColumnMappings.Add("Birthday", "Birthday");
sqlBulkCopy.ColumnMappings.Add("Gender", "Gender");
sqlBulkCopy.ColumnMappings.Add("Email", "Email");
sqlBulkCopy.ColumnMappings.Add("Deleted", "Deleted"); sqlBulkCopy.WriteToServer(dataTable);
}
transaction.Commit();
}
catch
{
if (transaction!=null)
{
transaction.Rollback();
}
throw;
}
}
} private static long InsertByEntityFramework()
{
Stopwatch stopwatch = Stopwatch.StartNew();
using (MyDbContext context = new MyDbContext(ConnectionString))
{
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
for (int i = ; i < Entries; i++)
{
context.Users.Add(new User()
{
Name = "name" + i,
Birthday = DateTime.Now.AddDays(i),
Gender = "F",
Email = "user" + i + "@abc.com",
Deleted = false
});
}
context.SaveChanges();
} stopwatch.Stop();
Console.WriteLine("EntityFramework time cost: {0}ms", stopwatch.ElapsedMilliseconds);
return stopwatch.ElapsedMilliseconds;
}
}
}

插入1000行测试结果:

插入10000行测试结果:

分析与结论:单从性能上来说,SqlBulkCopy和表参数StoreProcedure胜出,且完胜Entity Framework,所以当EF实在无法满足性能要求时,SqlBulkCopy或表参数SP可以很好的解决EF批量插入的性能问题。但衡量软件产品的标准不仅仅只有性能这一方面,比如我们还要在设计美学和性能之间进行权衡。当插入数据量较小或是低压力时间段自动执行插入的话,EF仍然是不错的选择。从代码可维护性方面来看ADO.NET实现的可读性、重构友好型都弱于EF实现,所以对于需求变动较多的领域模型而言这几种解决方法都需要更多的设计抽象和单元测试,以此来确保产品的持续发展。从影响范围来看,在ADO.NET实现方式中SqlBulkCopy和拼接Sql字符串的方案不需要额外加入存储过程,所以可以在不影响数据库部署的前提下与EF的实现相互替换。

关于SqlBulkCopy请参考:Bulk Copy Operations in SQL Server

为了比较优雅使用SqlBulkCopy,有人写了一种AsDataReader扩展方法请参考:LinqEntityDataReader

根据MSDN的说法,由于表参数存储过程的启动准备消耗时间较小,所以1k行(经验)以下插入性能将胜于SqlBulkCopy,而随着插入行数的增多,SqlBulkCopy的性能优势将体现出来,另外两种方案相比还有一些其他方面的差异,从本测试的实际结果来看,SqlBulkCopy在首次插入1k条数据时确实耗时稍长一点。具体请参考:Table-Valued Parameters vs. BULK INSERT Operations

另外还有人做过SqlBulkCopy和SqlDataAdapter插入的性能对比:High performance bulk loading to SQL Server using SqlBulkCopy

Entity Framework与ADO.NET批量插入数据性能测试的更多相关文章

  1. 传智播客--ADO.net--SqlBulkCopy批量插入数据(小白必知)

    一般情况下,我们在向数据库中插入数据时用Insert语句,但是当数据量很大的时候,这种情况就比较缓慢了,这个时候就需要SqlBulkCopy这个类. SqlBulkCopy本身常用的函数有这么几个 D ...

  2. C#批量插入数据到Sqlserver中的四种方式

    我的新书ASP.NET MVC企业级实战预计明年2月份出版,感谢大家关注! 本篇,我将来讲解一下在Sqlserver中批量插入数据. 先创建一个用来测试的数据库和表,为了让插入数据更快,表中主键采用的 ...

  3. C#批量插入数据到Sqlserver中的三种方式

    本篇,我将来讲解一下在Sqlserver中批量插入数据. 先创建一个用来测试的数据库和表,为了让插入数据更快,表中主键采用的是GUID,表中没有创建任何索引.GUID必然是比自增长要快的,因为你生 成 ...

  4. C#_批量插入数据到Sqlserver中的四种方式

    先创建一个用来测试的数据库和表,为了让插入数据更快,表中主键采用的是GUID,表中没有创建任何索引.GUID必然是比自增长要快的,因为你生成一个GUID算法所花的时间肯定比你从数据表中重新查询上一条记 ...

  5. MSSql Server 批量插入数据优化

    针对批量入库, .Net Framework  提供了一个批量入库Class : SqlBulkCopy , 批量入库性能不错,经测试 四万左右数据 2秒入库. 以下是测试Demo , 使用外部传入事 ...

  6. C#批量插入数据到Sqlserver中的四种方式 - 转

    先创建一个用来测试的数据库和表,为了让插入数据更快,表中主键采用的是GUID,表中没有创建任何索引.GUID必然是比自增长要快的,因为你生成一个GUID算法所花的时间肯定比你从数据表中重新查询上一条记 ...

  7. Oracle批量插入数据SQL语句太长出错:无效的主机/绑定变量名

    Oracle数据库,用mybatic批量插入数据: <insert id="saveBatch" parameterType="io.renren.entity.N ...

  8. 【转载】C#批量插入数据到Sqlserver中的三种方式

    引用:https://m.jb51.net/show/99543 这篇文章主要为大家详细介绍了C#批量插入数据到Sqlserver中的三种方式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 本篇, ...

  9. 【转载】MyBatis批量插入数据(insert)

    介绍:MyBatis批量插入数据,原理就是在xml文件中添加 foreach 语句,然后MyBatis自动在values后面添加多个括号: XML文件如下: <?xml version=&quo ...

随机推荐

  1. 使用ZwMapViewOfSection创建内存映射文件总结

    标 题: [原创]使用ZwMapViewOfSection创建内存映射文件总结 作 者: 小覃 时 间: 2012-06-15,02:28:36 链 接: http://bbs.pediy.com/s ...

  2. 3.View绘制分析笔记之onLayout

    上一篇文章我们了解了View的onMeasure,那么今天我们继续来学习Android View绘制三部曲的第二步,onLayout,布局. ViewRootImpl#performLayout pr ...

  3. Keras官方Example里Mnist-cnn的调试运行

    问题:老板让测试运行Keras官网里的Mnist-cnn.py,结果从下载数据就是一路坑-- 当前环境:Ubuntu12.04.python2.7.Keras 1.1.1(不知道这个版本号对不对,在启 ...

  4. 【原】iOS学习之Masonry第三方约束

    1.Masonry概述 目前最流行的Autolayout第三方框架 用优雅的代码方式编写Autolayout 省去了苹果官方恶心的Autolayout代码 大大提高了开发效率 框架地址:https:/ ...

  5. T-SQL Recipes之Database Backups

    The Problem 在DBA和T-SQL码奴日常工作中,比如常规检查,服务管理,数据库管理, 是其中最具挑战性的一个领域. 在相似任务中,比如索引碎片管理,统计管理,数据库备份是异常重要的,对任何 ...

  6. [翻译svg教程]svg中的circle元素

    svg中的<circle> 元素,是用来绘制圆形的,例如 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink= ...

  7. mvn使用笔记

    mvn命令格式: You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:< ...

  8. Python3.5 Day1作业:实现用户密码登录,输错三次锁定。

    作业需求: 1.输入用户名密码 2.认证成功后显示欢迎信息 3.输错三次后锁定 实现思路: 1.判断用户是否在黑名单,如果在黑名单提示账号锁定. 2.判断用户是否存在,如果不存在提示账号不存在. 3. ...

  9. 速度极快的导出excel

    public class Export2Excel { #region [导出文件,使用文件流] /// <summary> /// 导出文件,使用文件流.该方法使用的数据源为DataTa ...

  10. web应用程序

    1.web应用程序和网站的区别 应用程序有两种模式C/S.B/S.C/S是客户端/服务器端程序,也就是说这类程序一般独立运行.而B/S就是浏览器端/服务器端应用程序,这类应用程序一般借助IE等浏览器来 ...