MSSQL数据批量插入优化详细

序言 

如今有一个需求是将10w条数据插入到MSSQL数据库中,表结构以下,你会怎么作,你感受插入10W条数据插入到MSSQL以下的表中须要多久呢?算法

或者你的批量数据是如何插入的呢?我今天就此问题作个探讨。sql

 

压测mvc的http接口看下数据

首先说下这里只是作个参照,来理解插入数据库的性能情况,与开篇的需求无半毛钱关系。数据库

mvc接口代码以下:编程

public bool Add(CustomerFeedbackEntity m)
        {
            using (var conn=Connection)
            {
                string sql = @"INSERT INTO [dbo].[CustomerFeedback]
                                           ([BusType]
                                           ,[CustomerPhone]
                                           ,[BackType]
                                           ,[Content]
                                          )
                                     VALUES
                                           (@BusType
                                           ,@CustomerPhone
                                           ,@BackType
                                           ,@Content
                                           )";
                return conn.Execute(sql, m) > 0;
            }
        }

压测的此mvc接口单条数据插入数据库的聚合数据图。数组

用例这样的:5000个请求分500个线程执行post请求接口。缓存

这个图告诉咱们,最慢的请求只用啦4毫秒。那么咱们作个算法。安全

如开篇的需求来看,咱们用最小的响应时间来计算。服务器

那么插入10w条数据到数据库需用时=100000*4毫秒,大体是6.67分钟。那么咱们奔着这个目标来作出插入方案。session

最多见的insert作法

首先咱们的工程师拿到需求后这样写啦段代码,以下:mvc

//执行数据条数
        int cnt = 10 * 10000;
        //要插入的数据
        CustomerFeedbackEntity m = new CustomerFeedbackEntity() { BusType = 1, CustomerPhone = "1888888888", BackType = 1, Content = "123123dagvhkfhsdjk确定会撒娇繁华的撒娇防御等级划分噶哈苏德高房价盛大开放" };
        //第一种
        public void FristWay()
        {
            using (var conn = new SqlConnection(ConnStr))
            {
                conn.Open();
                Stopwatch sw = new Stopwatch();
                sw.Start();
                StringBuilder sb = new StringBuilder();
                Console.WriteLine("从:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "开始循环执行:" + cnt + "条sql语句 ...");
                for (int i = 0; i <= cnt; i++)
                {
                    sb.Clear();
                    sb.Append(@"INSERT INTO [dbo].[CustomerFeedback]
                                           ([BusType]
                                           ,[CustomerPhone]
                                           ,[BackType]
                                           ,[Content]
                                          )
                                     VALUES(");
                    sb.Append(m.BusType);
                    sb.Append(",'");
                    sb.Append(m.CustomerPhone);
                    sb.Append("',");
                    sb.Append(m.BackType);
                    sb.Append(",'");
                    sb.Append(m.Content);
                    sb.Append("')");
                    using (SqlCommand cmd = new SqlCommand(sb.ToString(), conn))
                    {
                        cmd.CommandTimeout = 0;
                        cmd.ExecuteNonQuery();
                    }
                }
                Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "时,执行:" + cnt + "条sql语句完成 ! 耗时:" + sw.ElapsedMilliseconds + "毫秒。");
            }
        }

执行结果以下:

10w条数据,693906毫秒,11分钟,有没有感受还行,或者还能够接受的。亲们,我是吐血状不说话,继续写,大家看MSSQL数据库与.Net配合插入止于哪里?

点评下:

一、不停的建立与释放sqlcommon对象,会有性能浪费。

二、不停的与数据库创建链接,会有很大的性能损耗。

此2点还有执行结果告诉咱们,此种方式不可取,即使这是咱们最多见的数据插入方式。

那么咱们针对以上两点作优化,一、建立一次sqlcommon对象,只与数据库创建一次链接。优化改造代码以下:

 public void SecondWay()
        {
            using (var conn = new SqlConnection(ConnStr))
            {
                conn.Open();
                Stopwatch sw = new Stopwatch();
                sw.Start();
                StringBuilder sb = new StringBuilder();
                Console.WriteLine("从:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "开始循环拼接:" + cnt + "条sql语句 ...");
                for (int i = 0; i <= cnt; i++)
                {
                    sb.Append(@"INSERT INTO [dbo].[CustomerFeedback]
                                           ([BusType]
                                           ,[CustomerPhone]
                                           ,[BackType]
                                           ,[Content]
                                          )
                                     VALUES(");
                    sb.Append(m.BusType);
                    sb.Append(",'");
                    sb.Append(m.CustomerPhone);
                    sb.Append("',");
                    sb.Append(m.BackType);
                    sb.Append(",'");
                    sb.Append(m.Content);
                    sb.Append("')");
                }
                var result = sw.ElapsedMilliseconds;
                Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "时,循环拼接:" + cnt + "条sql语句完成 ! 耗时:" + result + "毫秒。");
                using (SqlCommand cmd = new SqlCommand(sb.ToString(), conn))
                {
                    cmd.CommandTimeout = 0;
                    Stopwatch sw1 = new Stopwatch();
                    sw1.Start();
                    Console.WriteLine("从:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "开始执行:" + cnt + "条sql语句 ...");
                    cmd.ExecuteNonQuery();
                    Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "时,执行:" + cnt + "条sql语句完成 ! 耗时:" + sw1.ElapsedMilliseconds + "毫秒。");
                }
            }
        }

执行结果以下:

呀,好奇怪啊,为何跟上一个方案没有多大区别呢?

首先咱们看下拼接这么长的sql语句是怎么在数据库中是怎么执行的。

一、查看数据库的链接状况

select * from sysprocesses where dbid in (select dbid from sysdatabases where name='dbname')
--或者
SELECT * FROM
[Master].[dbo].[SYSPROCESSES] WHERE [DBID] IN ( SELECT 
   [DBID]
FROM 
   [Master].[dbo].[SYSDATABASES]
WHERE 
   NAME='dbname'
)

二、查看数据库正在执行的sql语句

SELECT  [Spid] = session_id ,
            ecid ,
            [Database] = DB_NAME(sp.dbid) ,
            [User] = nt_username ,
            [Status] = er.status ,
            [Wait] = wait_type ,
            [Individual Query] = SUBSTRING(qt.text,
                                           er.statement_start_offset / 2,
                                           ( CASE WHEN er.statement_end_offset = -1
                                                  THEN LEN(CONVERT(NVARCHAR(MAX), qt.text))
                                                       * 2
                                                  ELSE er.statement_end_offset
                                             END - er.statement_start_offset )
                                           / 2) ,
            [Parent Query] = qt.text ,
            Program = program_name ,
            hostname ,
            nt_domain ,
            start_time
    FROM    sys.dm_exec_requests er
            INNER JOIN sys.sysprocesses sp ON er.session_id = sp.spid
            CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) AS qt
    WHERE   session_id > 50 -- Ignore system spids.
            AND session_id NOT IN ( @@SPID ) -- Ignore this current statement.
ORDER BY    1 ,
            2

点评:虽然看似获得啦优化,其实与上一个解决方案的执行过程几乎是同样的,因此就不用多说什么啦。

利于MSSQL数据库的用户自定义表类型作优化

依旧先上代码,或许这样你才能对用户自定义表类型产生兴趣。

CREATE TYPE CustomerFeedbackTemp AS  TABLE( BusType int NOT NULL, CustomerPhone varchar(40) NOT NULL, BackType int NOT NULL, Content nvarchar(1000) NOT NULL )
 public void ThirdWay() { Stopwatch sw = new Stopwatch(); Stopwatch sw1 = new Stopwatch(); DataTable dt = GetTable(); using (var conn = new SqlConnection(ConnStr)) { string sql = @"INSERT INTO[dbo].[CustomerFeedback] ([BusType] ,[CustomerPhone] ,[BackType] ,[Content] ) select BusType,CustomerPhone,BackType,[Content] from @TempTb"; using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.CommandTimeout = 0; SqlParameter catParam = cmd.Parameters.AddWithValue("@TempTb", dt); catParam.SqlDbType = SqlDbType.Structured; catParam.TypeName = "dbo.CustomerFeedbackTemp"; conn.Open(); Console.WriteLine("从:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "开始循环插入内存表中:" + cnt + "条数据 ..."); sw.Start(); for (int i = 0; i < cnt; i++) { DataRow dr = dt.NewRow(); dr[0] = m.BusType; dr[1] = m.CustomerPhone; dr[2] = m.BackType; dr[3] = m.Content; dt.Rows.Add(dr); } Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "时,循环插入内存表:" + cnt + "条数据完成 ! 耗时:" + sw.ElapsedMilliseconds + "毫秒。"); sw1.Start(); if (dt != null && dt.Rows.Count != 0) { cmd.ExecuteNonQuery(); sw.Stop(); } Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "时,执行:" + cnt + "条数据的datatable的数据进数据库 ! 耗时:" + sw1.ElapsedMilliseconds + "毫秒。"); } } }

运行结果:

哇抓Q,不到2秒,不到2秒,怎么比每条4毫秒还快,不敢相信,是否是运行出问题啦。

再来一遍

再来一遍

是的你没有看错,10w条数据,不到2秒。是否是火烧眉毛的要知道为何?火烧眉毛的想知道咱们用到的用户自定义表类型是什么?

用户自定义表类型

首先类型你们应该很容易理解,像int,varchar,bit等都是类型,那么这个表类型是个毛线呢?

其实他就是用户能够本身定义一个表结构而后把他看成一个类型。

建立自定义类型的详细文档:https://msdn.microsoft.com/zh-cn/library/ms175007.aspx 

其次自定义类型也有一些限制,安全性:https://msdn.microsoft.com/zh-cn/library/bb522526.aspx

而后就是如何用这个类型,他的使用就是做为表值参数来使用的。

使用表值参数,能够没必要建立临时表或许多参数,便可向 Transact-SQL 语句或例程(如存储过程或函数)发送多行数据。

表值参数与 OLE DB 和 ODBC 中的参数数组相似,但具备更高的灵活性,且与 Transact-SQL 的集成更紧密。 表值参数的另外一个优点是可以参与基于数据集的操做。

Transact-SQL 经过引用向例程传递表值参数,以免建立输入数据的副本。 可使用表值参数建立和执行 Transact-SQL 例程,而且可使用任何托管语言从 Transact-SQL 代码、托管客户端以及本机客户端调用它们。

优势

就像其余参数同样,表值参数的做用域也是存储过程、函数或动态 Transact-SQL 文本。 一样,表类型变量也与使用 DECLARE 语句建立的其余任何局部变量同样具备做用域。 能够在动态 Transact-SQL 语句内声明表值变量,而且能够将这些变量做为表值参数传递到存储过程和函数。

表值参数具备更高的灵活性,在某些状况下,可比临时表或其余传递参数列表的方法提供更好的性能。 表值参数具备如下优点:

  • 首次从客户端填充数据时,不获取锁。

  • 提供简单的编程模型。

  • 容许在单个例程中包括复杂的业务逻辑。

  • 减小到服务器的往返。

  • 能够具备不一样基数的表结构。

  • 是强类型。

  • 使客户端能够指定排序顺序和惟一键。

  • 在用于存储过程时像临时表同样被缓存。 从 SQL Server 2012 开始,对于参数化查询,表值参数也被缓存。

限制

表值参数有下面的限制:

  • SQL Server 不维护表值参数列的统计信息。

  • 表值参数必须做为输入 READONLY 参数传递到 Transact-SQL 例程。 不能在例程体中对表值参数执行诸如 UPDATE、DELETE 或 INSERT 这样的 DML 操做。

  • 不能将表值参数用做 SELECT INTO 或 INSERT EXEC 语句的目标。 表值参数能够在 SELECT INTO 的 FROM 子句中,也能够在 INSERT EXEC 字符串或存储过程当中。

常见的BULK INSERT 数据集插入优化

public void FourWay()
        {

            Stopwatch sw = new Stopwatch();
            Stopwatch sw1 = new Stopwatch();
            DataTable dt = GetTable();
            using (SqlConnection conn = new SqlConnection(ConnStr))
            {
                SqlBulkCopy bulkCopy = new SqlBulkCopy(conn);
                bulkCopy.BulkCopyTimeout = 0;
                bulkCopy.DestinationTableName = "CustomerFeedback";
                bulkCopy.BatchSize = dt.Rows.Count;
                conn.Open();
                Console.WriteLine("从:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "开始循环插入内存表中:" + cnt + "条数据 ...");
                sw.Start();
                for (int i = 0; i < cnt; i++)
                {
                    DataRow dr = dt.NewRow();
                    dr[0] = m.BusType;
                    dr[1] = m.CustomerPhone;
                    dr[2] = m.BackType;
                    dr[3] = m.Content;
                    dt.Rows.Add(dr);
                }
                Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "时,循环插入内存表:" + cnt + "条数据完成 ! 耗时:" + sw.ElapsedMilliseconds + "毫秒。");
                sw1.Start();
                if (dt != null && dt.Rows.Count != 0)
                {
                    bulkCopy.WriteToServer(dt);
                    sw.Stop();
                }
                Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + "时,执行:" + cnt + "条数据的datatable的数据进数据库 ! 耗时:" + sw1.ElapsedMilliseconds + "毫秒。");
            }

执行结果:

1秒以内完成,1秒以内完成,看完这个简直要在1秒以内完成10w条数据的插入的节奏,逆天,逆天啊。

bulk insert详解:https://msdn.microsoft.com/zh-cn/library/ms188365.aspx

专业的点评:

表值参数的使用方法与其余基于数据集的变量的使用方法类似;可是,频繁使用表值参数将比大型数据集要快。 大容量操做的启动开销比表值参数大,与之相比,表值参数在插入数目少于 1000 的行时具备很好的执行性能。

重用的表值参数可从临时表缓存中受益。 这一表缓存功能可比对等的 BULK INSERT 操做提供更好的伸缩性。 使用小型行插入操做时,能够经过使用参数列表或批量语句(而不是 BULK INSERT 操做或表值参数)来得到小的性能改进。 可是,这些方法在编程上不太方便,而且随着行的增长,性能会迅速降低。

表值参数在执行性能上与对等的参数阵列实现至关甚至更好。

总结

接下来是你们最喜欢的总结内容啦,内容有三,以下:

一、但愿能关注我其余的文章。

二、博客里面有没有很清楚的说明白,或者你有更好的方式,那么欢迎加入左上方的2个交流群,咱们一块儿学习探讨。

三、你能够忘记点赞加关注,但千万不要忘记扫码打赏哦。

相关文章
相关标签/搜索