Contents

使用 TVP (Table Value Parameters) 實作快速大量更新

使用 TVP (Table Value Parameters) 實作快速大量更新

雖然早先就聽過 TVP (Table Value Parameters) 但一直沒有機會用到,這次難得遇到需要快速大量更新資料表的情景,就順手紀錄一下。

ORM 使用 Dapper 來做簡易展示,其實 Dapper 有第三方 Library 版本 Dapper.Plus,可以處理大量更新,且程式碼簡潔易懂,不過是需要付費的版本

環境

  • Dapper
  • LinqPad 6
  • Northwind 資料庫
  • SQL Server Management Studio (SSMS)
Note
使用 TVP 更新格式需要完全相容,為了簡易示範,已經提前拿掉所有關聯性的 FK 值

建立資料表值參數型別以及預存程序

  1. 使用 SSMS 建立資料表值參數型別

    /static/使用TVP實作快速大量更新_529cbc317ac642af9a4fed8f07e413d6/2021-05-12_15-17-14.png
  2. 建立名為 OrderTableType 的資料表類型

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    USE [Northwind]
    GO
    
    CREATE TYPE [dbo].[OrderTableType] AS TABLE(
    	[OrderID] int,
    	[CustomerID] nchar(5) ,
    	[EmployeeID] int,
    	[OrderDate] datetime,
    	[RequiredDate] datetime,
    	[ShippedDate] datetime,
    	[ShipVia] int,
    	[Freight] money,
    	[ShipName] nvarchar(40),
    	[ShipAddress] nvarchar(60),
    	[ShipCity] nvarchar(15),
    	[ShipRegion] nvarchar(15), 
    	[ShipPostalCode] nvarchar(10),
    	[ShipCountry] nvarchar(15)
    )
    GO
    
  3. 資料表重新整理,並查看類型是否成功建立

    /static/使用TVP實作快速大量更新_529cbc317ac642af9a4fed8f07e413d6/2021-05-12_15-24-36.png
  4. 接下來建立預存程序

    /static/使用TVP實作快速大量更新_529cbc317ac642af9a4fed8f07e413d6/2021-05-12_15-26-56.png
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    CREATE PROCEDURE usp_UpdateOrders 
    	(@tvpNewOrder [dbo].[OrderTableType] READONLY)
    AS
    BEGIN
    	UPDATE [dbo].[Orders]
    		SET [OrderID] = ut.OrderID
    			,[CustomerID] = ut.CustomerID
    			,[EmployeeID] = ut.EmployeeID
    			,[OrderDate] = ut.OrderDate
    			,[RequiredDate] = ut.RequiredDate
    			,[ShippedDate] = ut.ShippedDate
    			,[ShipVia] = ut.ShipVia
    			,[Freight] = ut.Freight
    			,[ShipName] = ut.ShipName
    			,[ShipAddress] = ut.ShipAddress
    			,[ShipCity] = ut.ShipCity
    			,[ShipRegion] = ut.ShipRegion
    			,[ShipPostalCode] = ut.ShipPostalCode
    			,[ShipCountry] = ut.ShipCountry
    		FROM [dbo].[Order] AS [bt] JOIN @tvpNewOrder AS [ut] 
    			ON [bt].[OrderID] = [ut].[OrderID]
    END
    GO
    

    依照微軟Docs的說明,應該將剛剛的 Type 設置為 READONLY,然後建立更新用的預存程序

  5. 重新整理一下就可以看到剛剛建立的預存程序

    /static/使用TVP實作快速大量更新_529cbc317ac642af9a4fed8f07e413d6/2021-05-12_16-11-12.png

使用 Dapper 更新資料表

將要更新的資料作成 DataTable,透過 Dapper 建立 TVP 參數,把它傳遞給 SQL Server,更新的語法與剛剛建立的預存程序中更新差不多,但需留意 Primary Key 是不能修改的

完整程式碼如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
void Main()
{
	// 將原資料 ShipName 欄位加上_1
	var updateOrders = Orders.AsEnumerable()
	.Select(x => new
	{
		OrderID = x.OrderID,
		CustomerID = x.CustomerID,
		EmployeeID = x.EmployeeID,
		OrderDate = x.OrderDate,
		RequiredDate = x.RequiredDate,
		ShippedDate = x.ShippedDate,
		ShipVia = x.ShipVia,
		Freight = x.Freight,
		ShipName = $"{x.ShipName}_1",
		ShipAddress = x.ShipAddress,
		ShipCity = x.ShipCity,
		ShipRegion = x.ShipRegion,
		ShipPostalCode = x.ShipPostalCode,
		ShipCountry = x.ShipCountry
	}).ToList();

	string json = Newtonsoft.Json.JsonConvert.SerializeObject(updateOrders);
	DataTable dt = Newtonsoft.Json.JsonConvert.DeserializeObject<DataTable>(json);
	
	// 搭配 JOIN 的語法來 Update 資料
	var sqlCommand = @"UPDATE [dbo].[Orders]
						SET [CustomerID] = ut.CustomerID
							,[EmployeeID] = ut.EmployeeID
							,[OrderDate] = ut.OrderDate
							,[RequiredDate] = ut.RequiredDate
							,[ShippedDate] = ut.ShippedDate
							,[ShipVia] = ut.ShipVia
							,[Freight] = ut.Freight
							,[ShipName] = ut.ShipName
							,[ShipAddress] = ut.ShipAddress
							,[ShipCity] = ut.ShipCity
							,[ShipRegion] = ut.ShipRegion
							,[ShipPostalCode] = ut.ShipPostalCode
							,[ShipCountry] = ut.ShipCountry
						FROM [dbo].[Orders] AS [bt] JOIN @tvpNewOrder AS [ut] 
							ON [bt].[OrderID] = [ut].[OrderID]";

	// 只計算更新資料表的時間做比較
	Stopwatch stopWatch = new Stopwatch();
	stopWatch.Reset();
	stopWatch.Start();

	using (var tranScope = new TransactionScope())
	{
		using (var connection = this.Connection)
		{
			// 建立 TVP 參數
			var tvp = new DynamicParameters();
			tvp.AddDynamicParams(new { tvpNewOrder = dt.AsTableValuedParameter("OrderTableType") });
			
			connection.Execute(sqlCommand, tvp);
		}
		tranScope.Complete();
	}

	stopWatch.Stop();
	TimeSpan ts = stopWatch.Elapsed;
	string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
	elapsedTime.Dump();
}
/static/使用TVP實作快速大量更新_529cbc317ac642af9a4fed8f07e413d6/2021-05-12_17-15-06.png

比較

不使用 TVP 的傳統更新

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void Main()
{
	// 將原資料 ShipName 欄位加上_2
	var updateOrders = Orders.AsEnumerable()
	.Select(x => new
	{
		OrderID = x.OrderID,
		CustomerID = x.CustomerID,
		EmployeeID = x.EmployeeID,
		OrderDate = x.OrderDate,
		RequiredDate = x.RequiredDate,
		ShippedDate = x.ShippedDate,
		ShipVia = x.ShipVia,
		Freight = x.Freight,
		ShipName = $"{x.ShipName}_2",
		ShipAddress = x.ShipAddress,
		ShipCity = x.ShipCity,
		ShipRegion = x.ShipRegion,
		ShipPostalCode = x.ShipPostalCode,
		ShipCountry = x.ShipCountry
	}).ToList();

	var sqlCommand = @"UPDATE [dbo].[Orders]
							SET [CustomerID] = @CustomerID
								,[EmployeeID] = @EmployeeID
								,[OrderDate] = @OrderDate
								,[RequiredDate] = @RequiredDate
								,[ShippedDate] = @ShippedDate
								,[ShipVia] = @ShipVia
								,[Freight] = @Freight
								,[ShipName] = @ShipName
								,[ShipAddress] = @ShipAddress
								,[ShipCity] = @ShipCity
								,[ShipRegion] = @ShipRegion
								,[ShipPostalCode] = @ShipPostalCode
								,[ShipCountry] = @ShipCountry
							WHERE [OrderID] = @OrderID";
	
	// 只計算更新資料表的時間做比較
	Stopwatch stopWatch = new Stopwatch();
	stopWatch.Reset();
	stopWatch.Start();
	
	using (var tranScope = new TransactionScope())
	{
		using (var connection = this.Connection)
		{
			connection.Execute(sqlCommand, updateOrders);
		}
		tranScope.Complete();
	}

	stopWatch.Stop();
	TimeSpan ts = stopWatch.Elapsed;
	string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
	elapsedTime.Dump();
}
/static/使用TVP實作快速大量更新_529cbc317ac642af9a4fed8f07e413d6/2021-05-12_17-18-44.png

參考

資料表值參數

https://docs.microsoft.com/zh-tw/dotnet/framework/data/adonet/sql/table-valued-parameters#creating-table-valued-parameter-types

軟體主廚的程式料理廚房

用 SqlBulkCopy 可以快速批次 Insert 大量資料,那批次 Update 大量資料呢?

結論

手邊並沒有更合適的資料表能作為展示,僅能以北風資料庫做示範,比較性有限,實際用在更新五萬多筆且資料欄為龐大的資料表,速度差異非常明顯。