Entity Framework是.NET平台下的一种简单易用的ORM框架,它既便于Domain Model和持久层的OO设计,也提高了代码的可维护性。但在使用中发现,有几类业务场景是EF不太擅长的,比如批量写入大量同类数据,为此本人做了一些对比测试,以供大家参考。
现假设我们需要做一个用户批量导入的功能,需要从某处导入1k~1w个User到SQLServer数据库,本人听说过的常见做法有如下几种:
- 使用ADO.NET单条SqlCommand执行1w次(根据常识作为EF的替代其性能还不够格,所以就不做测试了)
- 使用StringBuilder拼接SQL语句,将1w条Insert语句拼接成1到若干条SqlCommand执行
- 使用EntityFramework的基本功能进行插入
- 使用SqlBulkCopy进行批量插入
- 使用存储过程,其中的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批量插入数据性能测试的更多相关文章
-
传智播客--ADO.net--SqlBulkCopy批量插入数据(小白必知)
一般情况下,我们在向数据库中插入数据时用Insert语句,但是当数据量很大的时候,这种情况就比较缓慢了,这个时候就需要SqlBulkCopy这个类. SqlBulkCopy本身常用的函数有这么几个 D ...
-
C#批量插入数据到Sqlserver中的四种方式
我的新书ASP.NET MVC企业级实战预计明年2月份出版,感谢大家关注! 本篇,我将来讲解一下在Sqlserver中批量插入数据. 先创建一个用来测试的数据库和表,为了让插入数据更快,表中主键采用的 ...
-
C#批量插入数据到Sqlserver中的三种方式
本篇,我将来讲解一下在Sqlserver中批量插入数据. 先创建一个用来测试的数据库和表,为了让插入数据更快,表中主键采用的是GUID,表中没有创建任何索引.GUID必然是比自增长要快的,因为你生 成 ...
-
C#_批量插入数据到Sqlserver中的四种方式
先创建一个用来测试的数据库和表,为了让插入数据更快,表中主键采用的是GUID,表中没有创建任何索引.GUID必然是比自增长要快的,因为你生成一个GUID算法所花的时间肯定比你从数据表中重新查询上一条记 ...
-
MSSql Server 批量插入数据优化
针对批量入库, .Net Framework 提供了一个批量入库Class : SqlBulkCopy , 批量入库性能不错,经测试 四万左右数据 2秒入库. 以下是测试Demo , 使用外部传入事 ...
-
C#批量插入数据到Sqlserver中的四种方式 - 转
先创建一个用来测试的数据库和表,为了让插入数据更快,表中主键采用的是GUID,表中没有创建任何索引.GUID必然是比自增长要快的,因为你生成一个GUID算法所花的时间肯定比你从数据表中重新查询上一条记 ...
-
Oracle批量插入数据SQL语句太长出错:无效的主机/绑定变量名
Oracle数据库,用mybatic批量插入数据: <insert id="saveBatch" parameterType="io.renren.entity.N ...
-
【转载】C#批量插入数据到Sqlserver中的三种方式
引用:https://m.jb51.net/show/99543 这篇文章主要为大家详细介绍了C#批量插入数据到Sqlserver中的三种方式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 本篇, ...
-
【转载】MyBatis批量插入数据(insert)
介绍:MyBatis批量插入数据,原理就是在xml文件中添加 foreach 语句,然后MyBatis自动在values后面添加多个括号: XML文件如下: <?xml version=&quo ...
随机推荐
-
js给数组去重写法
数组为 var list =['A','B','A']; 法一:常规做法,新建list,给list添加元素,添加前判断是否包含 var removeRepeatItem = function(list ...
-
asp.Net获取脚本传过来的参数的方法汇总
最基础的知识啦,不过,还是记下来吧. 接收用get 方法传输的数据的写法: string userName= Request.QueryString["name"]; 接收用pos ...
-
Json.Net
下载地址:Json.NET 文档地址:Json.NET Documentation 基本的序列化与反序列化 public class Product { public string Name { ge ...
-
gulp详细入门教程(转载)
本文转载自: gulp详细入门教程
-
fwite写入文件
用双引号(")定义字符串,PHP 懂得更多特殊字符的转义序列: 转移序列 说明 \n 换行 \r 回车 \t 水平制表符 \[/td> 反斜线 \$ 美元符号 \" 双引号 ...
-
linux vim taglist config
"vim config .vimrc "taglist map <silent> <F10> :TlistToggle let tlist_php_sett ...
-
[干货来袭]C#7.0新特性(VS2017可用)
前言 微软昨天发布了新的VS 2017 ..随之而来的还有很多很多东西... .NET新版本 ASP.NET新版本...等等..太多..实在没消化.. 分享一下其实2016年12月就已经公布了的C#7 ...
-
Sargable 与 谓语下推 (predicate pushdown) 简介
关键词:SQL优化 , sargable , pushdown filter , predicate pushdown Sargable Sargable = Search ARGument ABLE ...
-
Django之Cookie、Session、CSRF、Admin
Django之Cookie.Session.CSRF.Admin Cookie 1.获取Cookie: 1 2 3 4 5 6 request.COOKIES['key'] request.get ...
-
DevExpress WinForms使用教程:SVG图库和Image Picker
[DevExpress WinForms v18.2下载] 每个新版本都在几个新控件中引入了矢量图标支持. 对于v18.2,这是列表: BackstageViewControl及其项目 RecentI ...