如今有一個需求是將10w條數據插入到MSSQL數據庫中,表結構以下,你會怎麼作,你感受插入10W條數據插入到MSSQL以下的表中須要多久呢?算法
或者你的批量數據是如何插入的呢?我今天就此問題作個探討。sql
首先說下這裏只是作個參照,來理解插入數據庫的性能情況,與開篇的需求無半毛錢關係。數據庫
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
首先咱們的工程師拿到需求後這樣寫啦段代碼,以下: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
點評:雖然看似獲得啦優化,其實與上一個解決方案的執行過程幾乎是同樣的,因此就不用多說什麼啦。
依舊先上代碼,或許這樣你才能對用戶自定義表類型產生興趣。
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 字符串或存儲過程當中。
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個交流羣,咱們一塊兒學習探討。
三、你能夠忘記點贊加關注,但千萬不要忘記掃碼打賞哦。