[Azure][SQL]改善大量資料塞入效能低弱的案例分享

原本在地端可以很順利執行的應用程式,移到雲端後的效能怎麼變成了一場惡夢

前一陣子在跟朋友討論程式撰寫問題的時候,對於要用 Dapper 還是改用 SQL 來做處理,各自有不同的堅持和意見,但因為沒有實際的數據,因此還是無法說服朋友改用 SQL 的 Stored Procedure 來做大量的資料處理。

剛好這個周末在玩 ChatGPT 的時候,就突發奇想說要不然看 ChatGPT 可否自動幫我產生相對應的程式碼,這樣不就可以很快地來做一個比較嗎 ?

因此我就很快的先在我的資料庫建立一個資料表,接下來就用以下的 SQL 語法來做測試,其實這段語法很單純,就是產生 10000 筆資料,然後搭配 Dapper 的擴充,我們一次來做 10000 筆資料的新增

private void InsertData(string connectstr)
{
    IDbConnection db = new SqlConnection(connectstr);
    db.Open();
    var people = Enumerable.Range(1, 10000)
        .Select(i => new Person { Id = Guid.NewGuid(), Name = $"Person{i}", Description = Guid.NewGuid().ToString() })
        .ToList();
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();

    db.Execute("INSERT INTO Person (Id, Name,Description) VALUES (@Id, @Name,@Description)", people);
    db.Close();
    stopwatch.Stop();

    Console.Writeln( $"執行時間: {stopwatch.ElapsedMilliseconds} 毫秒" );
}

而搭配上述的指令,我同時在地端的 SQL Server 和雲端的 Azure SQL Database 上面建立同樣的資料表來做測試,因此測試出來的數據平均如下

 Local SQL ServerAzure SQL Database S0Azure SQL Database S3
時間(毫秒)2004337450315616

從數據上看起來,我們在 Azure SQL Database 雖然 DTU 已經從 S0 (10 DTU) 放大到 S3 (100 DTU),效能上雖然有一點提升,但是遠比地端 SQL Server 上寫入的速度還是相差非常多。但是為什麼會是這樣呢 ? 基本上我們可以在地端的 SQL Server 開啟 SQL Profiler 攔一下相關的 SQL 指令

從下圖中可以看出來,當我們雖然一次給要新增 10000 筆資料,但會變成應用程式要透過網路下 10000 筆 INSERT 的指令,因此在這樣的測試中,網路還是影響很大的測試數據

因此我們又將原本的測試,把他改成 Web API 的方式,這樣 Azure Web App 和 Azure SQL Database 都位在同一個地區,這樣網路速度應該可以避免掉,因此又在重作一次測試,把數據整理一下

時間(毫秒)Local SQL ServerAzure SQL Database S0Azure SQL Database S3
地端 WinForm2004337450315616
雲端 Web API 6302448811

這樣速度看起來雖然因為沒有受到網路的延遲而造成的影響,但整體數據上看起來,還是跟我們目標的時間還是有點落差,因此這個時候我們再次使用 ChatGPT , 請他將我們原本使用 Dapper 的範例,改成透過 Bulk Insert 的方式來做處理,看看是否會有所改善。而此時 ChatGTP 居然提供一段令我驚豔的程式碼,透過下面這段的擴充方法,將原本的 List 的資料可以轉換成 DataTable , 這樣就可以搭配 SqlBulkInsert 的來做處理

public static class Extensions
{
    public static DataTable ToDataTable<T>(this IEnumerable<T> items)
    {
        var properties = typeof(T).GetProperties();
        var dataTable = new DataTable();
        foreach (var property in properties)
        {
            dataTable.Columns.Add(property.Name, property.PropertyType);
        }
        foreach (var item in items)
        {
            var values = properties.Select(p => p.GetValue(item)).ToArray();
            dataTable.Rows.Add(values);
        }
        return dataTable;
    }
}

因此我的程式碼被調整成為這樣

public async Task<ActionResult> BulkInsert()
{
    var person = GetSampleData();
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    using (var bulkCopy = new SqlBulkCopy(_config.GetConnectionString("DefaultConnection")))
    {
        bulkCopy.DestinationTableName = "Person";
        bulkCopy.WriteToServer(person.ToDataTable());
    }
    stopwatch.Stop();
    return Ok(new { Result = person.Count , Time = stopwatch.ElapsedMilliseconds });
}

而透過這樣的處理,當我們跟原本使用 Dapper 去塞入 10000 筆資料來做比對

 Azure SQL Database S0Azure SQL Database S3
使用 Dapper Execute6302448811
使用 SQL Bulk Insert 42761076

這樣的處理在效能上就能有大幅的改善了。但如果不要用 Bulk Insert 的指令 , 而是用 Stored Procedure 內包成一個交易去填入資料,那其實效果跟 Bulk Insert 的指令差不多,有興趣的朋友可以參考以下的指令來測試看看

SET NOCOUNT ON

DECLARE @Table Table
(	
	Id		uniqueidentifier,
	Name	NCHAR(20),
	Description NCHAR(100)
)

DECLARE @PTR INT = 1
WHILE @PTR <= 10000
BEGIN
	INSERT INTO @Table VALUES ( NewID(), 'Person'+CAST(@PTR AS VARCHAR) , NewID() )
	SET @PTR += 1
END

INSERT INTO Person SELECT * FROM @Table