在SQL Server 中插入一條數據使用Insert語句,但是如果想要批量插入一堆數據的話,循環使用Insert不僅效率低,而且會導致SQL一系統性能問題。下面介紹SQL Server支持的兩種批量數據插入方法:Bulk和表值參數(Table-Valued Parameters)。
運行下面的腳本,建立測試數據庫和表值參數。
- --Create DataBase
- create database BulkTestDB;
- go
- use BulkTestDB;
- go
- --Create Table
- Create table BulkTestTable(
- Id int primary key,
- UserName nvarchar(32),
- Pwd varchar(16))
- go
- --Create Table Valued
- CREATE TYPE BulkUdt AS TABLE
- (Id int,
- UserName nvarchar(32),
- Pwd varchar(16))
下面我們使用最簡單的Insert語句來插入100萬條數據,代碼如下:
- Stopwatch sw = new Stopwatch();
- SqlConnection sqlConn = new SqlConnection(
- ConfigurationManager.ConnectionStrings["ConnStr"].ConnectionString);//連接數據庫
- SqlCommand sqlComm = new SqlCommand();
- sqlComm.CommandText = string.Format("insert into BulkTestTable(Id,UserName,Pwd)values(@p0,@p1,@p2)");//參數化SQL
- sqlComm.Parameters.Add("@p0", SqlDbType.Int);
- sqlComm.Parameters.Add("@p1", SqlDbType.NVarChar);
- sqlComm.Parameters.Add("@p2", SqlDbType.VarChar);
- sqlComm.CommandType = CommandType.Text;
- sqlComm.Connection = sqlConn;
- sqlConn.Open();
- try
- {
- //循環插入100萬條數據,每次插入10萬條,插入10次。
- for (int multiply = 0; multiply < 10; multiply++)
- {
- for (int count = multiply * 100000; count < (multiply + 1) * 100000; count++)
- {
- sqlComm.Parameters["@p0"].Value = count;
- sqlComm.Parameters["@p1"].Value = string.Format("User-{0}", count * multiply);
- sqlComm.Parameters["@p2"].Value = string.Format("Pwd-{0}", count * multiply);
- sw.Start();
- sqlComm.ExecuteNonQuery();
- sw.Stop();
- }
- //每插入10萬條數據后,顯示此次插入所用時間
- Console.WriteLine(string.Format("Elapsed Time is {0} Milliseconds", sw.ElapsedMilliseconds));
- }
- }
- catch (Exception ex)
- {
- throw ex;
- }
- finally
- {
- sqlConn.Close();
- }
- Console.ReadLine();
耗時圖如下:
由於運行過慢,才插入10萬條就耗時72390 milliseconds,所以我就手動強行停止了。
下面看一下使用Bulk插入的情況:
bulk方法主要思想是通過在客戶端把數據都緩存在Table中,然后利用SqlBulkCopy一次性把Table中的數據插入到數據庫
代碼如下:
- public static void BulkToDB(DataTable dt)
- {
- SqlConnection sqlConn = new SqlConnection(
- ConfigurationManager.ConnectionStrings["ConnStr"].ConnectionString);
- SqlBulkCopy bulkCopy = new SqlBulkCopy(sqlConn);
- bulkCopy.DestinationTableName = "BulkTestTable";
- bulkCopy.BatchSize = dt.Rows.Count;
- try
- {
- sqlConn.Open();
- if (dt != null && dt.Rows.Count != 0)
- bulkCopy.WriteToServer(dt);
- }
- catch (Exception ex)
- {
- throw ex;
- }
- finally
- {
- sqlConn.Close();
- if (bulkCopy != null)
- bulkCopy.Close();
- }
- }
- public static DataTable GetTableSchema()
- {
- DataTable dt = new DataTable();
- dt.Columns.AddRange(new DataColumn[]{
- new DataColumn("Id",typeof(int)),
- new DataColumn("UserName",typeof(string)),
- new DataColumn("Pwd",typeof(string))});
- return dt;
- }
- static void Main(string[] args)
- {
- Stopwatch sw = new Stopwatch();
- for (int multiply = 0; multiply < 10; multiply++)
- {
- DataTable dt = Bulk.GetTableSchema();
- for (int count = multiply * 100000; count < (multiply + 1) * 100000; count++)
- {
- DataRow r = dt.NewRow();
- r[0] = count;
- r[1] = string.Format("User-{0}", count * multiply);
- r[2] = string.Format("Pwd-{0}", count * multiply);
- dt.Rows.Add(r);
- }
- sw.Start();
- Bulk.BulkToDB(dt);
- sw.Stop();
- Console.WriteLine(string.Format("Elapsed Time is {0} Milliseconds", sw.ElapsedMilliseconds));
- }
- Console.ReadLine();
- }
耗時圖如下:
可見,使用Bulk后,效率和性能明顯上升。使用Insert插入10萬數據耗時72390,而現在使用Bulk插入100萬數據才耗時17583。
最后再看看使用表值參數的效率,會另你大為驚訝的。
表值參數是SQL Server 2008新特性,簡稱TVPs。對於表值參數不熟悉的朋友,可以參考最新的book online,我也會另外寫一篇關於表值參數的博客,不過此次不對表值參數的概念做過多的介紹。言歸正傳,看代碼:
- public static void TableValuedToDB(DataTable dt)
- {
- SqlConnection sqlConn = new SqlConnection(
- ConfigurationManager.ConnectionStrings["ConnStr"].ConnectionString);
- const string TSqlStatement =
- "insert into BulkTestTable (Id,UserName,Pwd)" +
- " SELECT nc.Id, nc.UserName,nc.Pwd" +
- " FROM @NewBulkTestTvp AS nc";
- SqlCommand cmd = new SqlCommand(TSqlStatement, sqlConn);
- SqlParameter catParam = cmd.Parameters.AddWithValue("@NewBulkTestTvp", dt);
- catParam.SqlDbType = SqlDbType.Structured;
- //表值參數的名字叫BulkUdt,在上面的建立測試環境的SQL中有。
- catParam.TypeName = "dbo.BulkUdt";
- try
- {
- sqlConn.Open();
- if (dt != null && dt.Rows.Count != 0)
- {
- cmd.ExecuteNonQuery();
- }
- }
- catch (Exception ex)
- {
- throw ex;
- }
- finally
- {
- sqlConn.Close();
- }
- }
- public static DataTable GetTableSchema()
- {
- DataTable dt = new DataTable();
- dt.Columns.AddRange(new DataColumn[]{
- new DataColumn("Id",typeof(int)),
- new DataColumn("UserName",typeof(string)),
- new DataColumn("Pwd",typeof(string))});
- return dt;
- }
- static void Main(string[] args)
- {
- Stopwatch sw = new Stopwatch();
- for (int multiply = 0; multiply < 10; multiply++)
- {
- DataTable dt = TableValued.GetTableSchema();
- for (int count = multiply * 100000; count < (multiply + 1) * 100000; count++)
- {
- DataRow r = dt.NewRow();
- r[0] = count;
- r[1] = string.Format("User-{0}", count * multiply);
- r[2] = string.Format("Pwd-{0}", count * multiply);
- dt.Rows.Add(r);
- }
- sw.Start();
- TableValued.TableValuedToDB(dt);
- sw.Stop();
- Console.WriteLine(string.Format("Elapsed Time is {0} Milliseconds", sw.ElapsedMilliseconds));
- }
- Console.ReadLine();
- }
耗時圖如下:
比Bulk還快5秒。
SQLBulkCopy,用於數據庫之間大批量的數據傳遞。通常用於新,舊數據庫之間數據的更新。即使表結構完全不同,也可以通過字段間的對應關系,順利的將數據導過來。
首先,SQLBulkCopy需要2個連接。分別連接到不同的舊表所在的數據庫,新表所在的數據庫。
其次,我們要從舊數據庫中,把導出的字段讀取出來。用什么讀呢?可以用Datatable,也可以用SqlDataReader。因為SqlDataReader不占用內存,對大批量的數據復制,不需要事先導入到系統。所以就用SqlDataReader了。
讀出后,設定對應關系,設定目標表名,寫入。就這么簡單。速度非常快!
初始化Connection對象
SqlConnection ConnectionNew=new SqlConnection("連接信息");
SqlConnection ConnectionOld=new SqlConnection("連接信息");
try
{
//1.在舊表中,用SqlDataReader讀取出信息
SqlCommand cmd = new SqlCommand(SQL, ConnectionOld);
sdr = cmd.ExecuteReader();
//2.初始化SqlBulkCopy對象,用新的連接作為參數。
SqlBulkCopy bulkCopy = new SqlBulkCopy(ConnectionNew);
//3.寫對應關系。如舊表的People列的數據,對應新表Human列,那么就寫bulkCopy.ColumnMappings.Add("People","Human")
//如果兩張表的結構一樣,那么對應關系就不用寫了。
//我是用哈希表存儲對應關系的,哈希表作為參數到傳入方法中,key的值用來存儲舊表的字段名,VALUE的值用來存儲新表的值
foreach (string str in HTDuiYing.Keys)
{
bulkCopy.ColumnMappings.Add(str, HTDuiYing[str].ToString());
}
//4.設置目標表名
bulkCopy.DestinationTableName = TableNmae;
//額外,可不寫:設置一次性處理的行數。這個行數處理完后,會激發SqlRowsCopied()方法。默認為1
bulkCopy.NotifyAfter = 10;
//額外,可不寫:設置激發的SqlRowsCopied()方法,這里為bulkCopy_SqlRowsCopied
bulkCopy.SqlRowsCopied += new SqlRowsCopiedEventHandler(bulkCopy_SqlRowsCopied);
//OK,開始傳數據!
bulkCopy.WriteToServer(sdr);
}
//激發的方法寫在外頭
private void bulkCopy_SqlRowsCopied(object sender, SqlRowsCopiedEventArgs e)
{
執行的內容。
這里有2個元素值得拿來用
e.RowsCopied,返回數值類型,表示當前已經復制的行數
e.Abort,用於賦值true or false,用於停止賦值的操作
}
由於不同批次在不同事務中執行,因此,如果在批量復制操作期間發生錯誤,則當前批次中的所有行都將被回滾,但以前批次中的行將保留在數據庫中。
比如:批量復制100條數據到數據庫匯總,batchsize設置為10.則沒10條數據復制作為一個事務,整個100條數據的復制操作被分割為10個獨立的事務。如果復制到第56條數據時(第6個事務),出錯了。則前50條數據提交到數據庫中,只回滾出錯的事務。
如果由於發生錯誤而需要回滾整個批量復制操作,或者批量復制應作為更大的可回滾進程的一部分執行,則可以將 SQLTransaction 對象提供給 SqlBulkCopy 構造函數.
示例:
using (SqlConnection destinationConnection = new SqlConnection(connectionString))
{
destinationConnection.Open();
using (SqlTransaction transaction = destinationConnection.BeginTransaction())
{
using (SqlBulkCopy bulkCopy = new SqlBulkCopy( destinationConnection, SqlBulkCopyOptions.Default, transaction))
{
bulkCopy.BatchSize = 10;
bulkCopy.DestinationTableName = "dbo.BulkCopyDemoMatchingColumns";
try
{
bulkCopy.WriteToServer(reader);
transaction.Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
transaction.Rollback();
}
finally
{
reader.Close();
}
}
}
}