本篇,我將來(lái)講解一下在Sqlserver中批量插入數(shù)據(jù)。
先創(chuàng)建一個(gè)用來(lái)測(cè)試的數(shù)據(jù)庫(kù)和表,為了讓插入數(shù)據(jù)更快,表中主鍵采用的是GUID,表中沒(méi)有創(chuàng)建任何索引。GUID必然是比自增長(zhǎng)要快的,因?yàn)槟闵梢粋€(gè)GUID算法所花的時(shí)間肯定比你從數(shù)據(jù)表中重新查詢上一條記錄的ID的值然后再進(jìn)行加1運(yùn)算要少。而如果存在索引的情況下,每次插入記錄都會(huì)進(jìn)行索引重建,這是非常耗性能的。如果表中無(wú)可避免的存在索引,我們可以通過(guò)先刪除索引,然后批量插入,最后再重建索引的方式來(lái)提高效率。
create database CarSYS; go use CarSYS; go CREATE TABLE Product(Id UNIQUEIDENTIFIER PRIMARY KEY,NAME VARCHAR(50) NOT NULL,Price DECIMAL(18,2) NOT NULL)
我們通過(guò)SQL腳本來(lái)插入數(shù)據(jù),常見(jiàn)如下四種方式。
方式一:一條一條插入,性能最差,不建議使用。
INSERT INTO Product(Id,Name,Price) VALUES(newid(),'牛欄1段',160);INSERT INTO Product(Id,Name,Price) VALUES(newid(),'牛欄2段',260);......
方式二:insert bulk
語(yǔ)法如下:
BULK INSERT [ [ 'database_name'.][ 'owner' ].]{ 'table_name' FROM 'data_file' }  WITH (   [ BATCHSIZE [ = batch_size ] ],   [ CHECK_CONSTRAINTS ],    [ CODEPAGE [ = 'ACP' | 'OEM' | 'RAW' | 'code_page' ] ],   [ DATAFILETYPE [ = 'char' | 'native'| 'widechar' | 'widenative' ] ],    [ FIELDTERMINATOR [ = 'field_terminator' ] ],   [ FIRSTROW [ = first_row ] ],   [ FIRE_TRIGGERS ],   [ FORMATFILE = 'format_file_path' ],   [ KEEPIDENTITY ],   [ KEEPNULLS ],   [ KILOBYTES_PER_BATCH [ = kilobytes_per_batch ] ],   [ LASTROW [ = last_row ] ],   [ MAXERRORS [ = max_errors ] ],   [ ORDER ( { column [ ASC | DESC ] } [ ,...n ] ) ],   [ ROWS_PER_BATCH [ = rows_per_batch ] ],   [ ROWTERMINATOR [ = 'row_terminator' ] ],    [ TABLOCK ],  )相關(guān)參數(shù)說(shuō)明:
 BULK INSERT  [ database_name . [ schema_name ] . | schema_name . ] [ table_name | view_name ]  FROM 'data_file'  [ WITH  (  [ [ , ] BATCHSIZE = batch_size ] --BATCHSIZE指令來(lái)設(shè)置在單個(gè)事務(wù)中可以插入到表中的記錄的數(shù)量  [ [ , ] CHECK_CONSTRAINTS ] --指定在大容量導(dǎo)入操作期間,必須檢查所有對(duì)目標(biāo)表或視圖的約束。若沒(méi)有 CHECK_CONSTRAINTS 選項(xiàng),則所有 CHECK 和 FOREIGN KEY 約束都將被忽略,并且在此操作之后表的約束將標(biāo)記為不可信。  [ [ , ] CODEPAGE = { 'ACP' | 'OEM' | 'RAW' | 'code_page' } ] --指定該數(shù)據(jù)文件中數(shù)據(jù)的代碼頁(yè)  [ [ , ] DATAFILETYPE =  { 'char' | 'native'| 'widechar' | 'widenative' } ] --指定 BULK INSERT 使用指定的數(shù)據(jù)文件類(lèi)型值執(zhí)行導(dǎo)入操作。  [ [ , ] FIELDTERMINATOR = 'field_terminator' ] --標(biāo)識(shí)分隔內(nèi)容的符號(hào)  [ [ , ] FIRSTROW = first_row ] --指定要加載的第一行的行號(hào)。默認(rèn)值是指定數(shù)據(jù)文件中的第一行  [ [ , ] FIRE_TRIGGERS ] --是否啟動(dòng)觸發(fā)器  [ [ , ] FORMATFILE = 'format_file_path' ]  [ [ , ] KEEPIDENTITY ] --指定導(dǎo)入數(shù)據(jù)文件中的標(biāo)識(shí)值用于標(biāo)識(shí)列  [ [ , ] KEEPNULLS ] --指定在大容量導(dǎo)入操作期間空列應(yīng)保留一個(gè)空值,而不插入用于列的任何默認(rèn)值  [ [ , ] KILOBYTES_PER_BATCH = kilobytes_per_batch ]  [ [ , ] LASTROW = last_row ] --指定要加載的最后一行的行號(hào)  [ [ , ] MAXERRORS = max_errors ] --指定允許在數(shù)據(jù)中出現(xiàn)的最多語(yǔ)法錯(cuò)誤數(shù),超過(guò)該數(shù)量后將取消大容量導(dǎo)入操作。  [ [ , ] ORDER ( { column [ ASC | DESC ] } [ ,...n ] ) ] --指定數(shù)據(jù)文件中的數(shù)據(jù)如何排序  [ [ , ] ROWS_PER_BATCH = rows_per_batch ]  [ [ , ] ROWTERMINATOR = 'row_terminator' ] --標(biāo)識(shí)分隔行的符號(hào)  [ [ , ] TABLOCK ] --指定為大容量導(dǎo)入操作持續(xù)時(shí)間獲取一個(gè)表級(jí)鎖  [ [ , ] ERRORFILE = 'file_name' ] --指定用于收集格式有誤且不能轉(zhuǎn)換為 OLE DB 行集的行的文件。  )]方式三:INSERT INTO xx select...
INSERT INTO Product(Id,Name,Price) SELECT NEWID(),'牛欄1段',160 UNION ALL SELECT NEWID(),'牛欄2段',180 UNION ALL......
方式四:拼接SQL
INSERT INTO Product(Id,Name,Price) VALUES(newid(),'牛欄1段',160),(newid(),'牛欄2段',260)......
在C#中通過(guò)ADO.NET來(lái)實(shí)現(xiàn)批量操作存在四種與之對(duì)應(yīng)的方式。
方式一:逐條插入
#region 方式一 static void InsertOne() {  Console.WriteLine("采用一條一條插入的方式實(shí)現(xiàn)");  Stopwatch sw = new Stopwatch();  using (SqlConnection conn = new SqlConnection(StrConnMsg)) //using中會(huì)自動(dòng)Open和Close 連接。  {  string sql = "INSERT INTO Product(Id,Name,Price) VALUES(newid(),@p,@d)";  conn.Open();  for (int i = 0; i < totalRow; i++)  {   using (SqlCommand cmd = new SqlCommand(sql, conn))   {   cmd.Parameters.AddWithValue("@p", "商品" + i);   cmd.Parameters.AddWithValue("@d", i);   sw.Start();   cmd.ExecuteNonQuery();   Console.WriteLine(string.Format("插入一條記錄,已耗時(shí){0}毫秒", sw.ElapsedMilliseconds));   }   if (i == getRow)   {   sw.Stop();   break;   }  }  }  Console.WriteLine(string.Format("插入{0}條記錄,每{4}條的插入時(shí)間是{1}毫秒,預(yù)估總得插入時(shí)間是{2}毫秒,{3}分鐘", totalRow, sw.ElapsedMilliseconds, ((sw.ElapsedMilliseconds / getRow) * totalRow), GetMinute((sw.ElapsedMilliseconds / getRow * totalRow)), getRow)); } static int GetMinute(long l) {  return (Int32)l / 60000; }  #endregion運(yùn)行結(jié)果如下:

我們會(huì)發(fā)現(xiàn)插入100w條記錄,預(yù)計(jì)需要50分鐘時(shí)間,每插入一條記錄大概需要3毫秒左右。
方式二:使用SqlBulk
#region 方式二 static void InsertTwo() {  Console.WriteLine("使用Bulk插入的實(shí)現(xiàn)方式");  Stopwatch sw = new Stopwatch();  DataTable dt = GetTableSchema();   using (SqlConnection conn = new SqlConnection(StrConnMsg))  {  SqlBulkCopy bulkCopy = new SqlBulkCopy(conn);  bulkCopy.DestinationTableName = "Product";  bulkCopy.BatchSize = dt.Rows.Count;  conn.Open();  sw.Start();  for (int i = 0; i < totalRow;i++ )  {   DataRow dr = dt.NewRow();   dr[0] = Guid.NewGuid();   dr[1] = string.Format("商品", i);   dr[2] = (decimal)i;   dt.Rows.Add(dr);  }   if (dt != null && dt.Rows.Count != 0)   {   bulkCopy.WriteToServer(dt);   sw.Stop();   }   Console.WriteLine(string.Format("插入{0}條記錄共花費(fèi){1}毫秒,{2}分鐘", totalRow, sw.ElapsedMilliseconds, GetMinute(sw.ElapsedMilliseconds)));  } } static DataTable GetTableSchema() {  DataTable dt = new DataTable();  dt.Columns.AddRange(new DataColumn[] {  new DataColumn("Id",typeof(Guid)),  new DataColumn("Name",typeof(string)),  new DataColumn("Price",typeof(decimal))});  return dt; } #endregion運(yùn)行結(jié)果如下:

插入100w條記錄才8s多,是不是很溜。
打開(kāi)Sqlserver Profiler跟蹤,會(huì)發(fā)現(xiàn)執(zhí)行的是如下語(yǔ)句:
insert bulk Product ([Id] UniqueIdentifier, [NAME] VarChar(50) COLLATE Chinese_PRC_CI_AS, [Price] Decimal(18,2))
方式三:使用TVPs(表值參數(shù))插入數(shù)據(jù)
從sqlserver 2008起開(kāi)始支持TVPs。創(chuàng)建緩存表ProductTemp ,執(zhí)行如下SQL。
CREATE TYPE ProductTemp AS TABLE(Id UNIQUEIDENTIFIER PRIMARY KEY,NAME VARCHAR(50) NOT NULL,Price DECIMAL(18,2) NOT NULL)
執(zhí)行完成之后,會(huì)發(fā)現(xiàn)在數(shù)據(jù)庫(kù)CarSYS下面多了一張緩存表ProductTemp


可見(jiàn)插入100w條記錄共花費(fèi)了11秒多。
方式四:拼接SQL
此種方法在C#中有限制,一次性只能批量插入1000條,所以就得分段進(jìn)行插入。
#region 方式四 static void InsertFour() {  Console.WriteLine("采用拼接批量SQL插入的方式實(shí)現(xiàn)");  Stopwatch sw = new Stopwatch();  using (SqlConnection conn = new SqlConnection(StrConnMsg)) //using中會(huì)自動(dòng)Open和Close 連接。  {  conn.Open();  sw.Start();  for (int j = 0; j < totalRow / getRow;j++ )  {   StringBuilder sb = new StringBuilder();   sb.Append("INSERT INTO Product(Id,Name,Price) VALUES");   using (SqlCommand cmd = new SqlCommand())   {    for (int i = 0; i < getRow; i++)   {    sb.AppendFormat("(newid(),'商品{0}',{0}),", j*i+i);   }   cmd.Connection = conn;   cmd.CommandText = sb.ToString().TrimEnd(',');   cmd.ExecuteNonQuery();   }  }  sw.Stop();  Console.WriteLine(string.Format("插入{0}條記錄,共耗時(shí){1}毫秒",totalRow,sw.ElapsedMilliseconds));  } } #endregion運(yùn)行結(jié)果如下:

我們可以看到大概花費(fèi)了10分鐘。雖然在方式一的基礎(chǔ)上,性能有了較大的提升,但是顯然還是不夠快。
總結(jié):大數(shù)據(jù)批量插入方式一和方式四盡量避免使用,而方式二和方式三都是非常高效的批量插入數(shù)據(jù)方式。其都是通過(guò)構(gòu)建DataTable的方式插入的,而我們知道DataTable是存在內(nèi)存中的,所以當(dāng)數(shù)據(jù)量特別特別大,大到內(nèi)存中無(wú)法一次性存儲(chǔ)的時(shí)候,可以分段插入。比如需要插入9千萬(wàn)條數(shù)據(jù),可以分成9段進(jìn)行插入,一次插入1千萬(wàn)條。而在for循環(huán)中直接進(jìn)行數(shù)據(jù)庫(kù)操作,我們是應(yīng)該盡量避免的。每一次數(shù)據(jù)庫(kù)的連接、打開(kāi)和關(guān)閉都是比較耗時(shí)的,雖然在C#中存在數(shù)據(jù)庫(kù)連接池,也就是當(dāng)我們使用using或者conn.Close(),進(jìn)行釋放連接時(shí),其實(shí)并沒(méi)有真正關(guān)閉數(shù)據(jù)庫(kù)連接,它只是讓連接以類(lèi)似于休眠的方式存在,當(dāng)再次操作的時(shí)候,會(huì)從連接池中找一個(gè)休眠狀態(tài)的連接,喚醒它,這樣可以有效的提高并發(fā)能力,減少連接損耗。而連接池中的連接數(shù),我們都是可以配置的。
以上就是本文的全部?jī)?nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,同時(shí)也希望多多支持武林網(wǎng)!
新聞熱點(diǎn)
疑難解答
圖片精選