2015-07-10 21 views
15

這個問題有很長的版本,並且是一個簡短的版本。C#,EF和LINQ:將大型(7Mb)記錄插入到SQL Server中很慢

短的版本:

爲什麼都是LINQ和EF在插入單個,大(7 MB)記錄到遠程SQL Server數據庫這麼慢?

而這裏的長版本(有關變通方法的一些信息,這可能是有用的其他讀者):

下面所有的示例代碼確實運行不錯,但因爲我的用戶是在歐洲而我們的數據中心總部位於美國,因此速度很慢。但是,如果我在美國的虛擬PC上運行相同的代碼,它會立即運行。 (並且,不幸的是,我的公司希望保留內部所有數據,所以我不能使用Azure,亞馬遜雲服務等)

我的很多公司應用都涉及從Excel讀取/寫入數據到SQL服務器,而且我們經常需要將Excel文件的原始副本保存在SQL Server表中。

這很簡單,只需從本地文件中讀取原始數據並將其保存到記錄中。

private int SaveFileToSQLServer(string filename) 
{ 
    // Read in an Excel file, and store it in a SQL Server [External_File] record. 
    // 
    // Returns the ID of the [External_File] record which was added. 
    // 

    DateTime lastModifed = System.IO.File.GetLastWriteTime(filename); 
    byte[] fileData = File.ReadAllBytes(filename); 

    // Create a new SQL Server database record, containing our file's raw data 
    // (Note: the table has an IDENTITY Primary-Key, so will generate a ExtFile_ID for us.) 
    External_File newFile = new External_File() 
    { 
     ExtFile_Filename = System.IO.Path.GetFileName(filename), 
     ExtFile_Data = fileData, 
     ExtFile_Last_Modified = lastModifed, 
     Update_By = "mike", 
     Update_Time = DateTime.UtcNow 
    }; 
    dc.External_Files.InsertOnSubmit(newFile); 
    dc.SubmitChanges(); 

    return newFile.ExtFile_ID; 
} 

是的,沒有什麼驚喜,它工作正常。

但是,我注意到,對於大型Excel文件(7-8Mb),插入一個(大!)記錄的代碼需要40-50秒才能運行。我把它放在一個後臺線程中,並且它一切正常,但是,當然,如果用戶退出我的應用程序,這個過程將被終止,這會導致問題。

作爲一個測試,我試着用代碼來做到這一點,以取代該功能:

  • 將文件複製到SQL Server機器
  • 調用的存儲過程來讀取原始數據上的共享目錄(blob)放入同一張表

使用此方法,整個過程只需要3-4秒。

如果你有興趣,這裏的存儲過程我用上傳文件(該文件必須存儲在SQL服務器本機上的文件夾中)到數據庫中的記錄:

CREATE PROCEDURE [dbo].[UploadFileToDatabase] 
    @LocalFilename nvarchar(400) 
AS 
BEGIN 
    -- By far, the quickest way to do this is to copy the file onto the SQL Server machine, then call this stored 
    -- procedure to read the raw data into a [External_File] record, and link it to the Pricing Account record. 
    -- 
    --  EXEC [dbo].[UploadPricingToolFile] 'D:\ImportData\SomeExcelFile.xlsm' 
    -- 
    -- Returns: -1 if something went wrong (eg file didn't exist) or the ID of our new [External_File] record 
    -- 
    -- Note that the INSERT will go wrong, if the user doesn't have "bulkadmin" rights. 
    --  "You do not have permission to use the bulk load statement." 
    -- EXEC master..sp_addsrvrolemember @loginame = N'GPP_SRV', @rolename = N'bulkadmin' 
    -- 
    SET NOCOUNT ON; 

    DECLARE 
     @filename nvarchar(300),  -- eg "SomeFilename.xlsx" (without the path) 
     @SQL nvarchar(2000), 
     @New_ExtFile_ID int 

    -- Extract (just) the filename from our Path+Filename parameter 
    SET @filename = RIGHT(@LocalFilename,charindex('\',reverse(@LocalFilename))-1) 

    SET @SQL = 'INSERT INTO [External_File] ([ExtFile_Filename], [ExtFile_Data]) ' 
    SET @SQL = @SQL + 'SELECT ''' + @Filename + ''', * 
    SET @SQL = @SQL + ' FROM OPENROWSET(BULK ''' + @LocalFilename +''', SINGLE_BLOB) rs' 

    PRINT convert(nvarchar, GetDate(), 108) + ' Running: ' + @SQL 
    BEGIN TRY 
     EXEC (@SQL) 
     SELECT @New_ExtFile_ID = @@IDENTITY 
    END TRY 
    BEGIN CATCH 
     PRINT convert(nvarchar, GetDate(), 108) + ' An exception occurred.' 
     SELECT -1 
     RETURN 
    END CATCH 

    PRINT convert(nvarchar, GetDate(), 108) + ' Finished.' 

    -- Return the ID of our new [External_File] record 
    SELECT @New_ExtFile_ID 
END 

的關鍵這段代碼,它建立了這樣一個SQL命令:

INSERT INTO [External_File] ([ExtFile_Filename], [ExtFile_Data]) 
SELECT 'SomeFilename.xlsm', * FROM OPENROWSET(BULK N'D:\ImportData\SomeExcelFile.xlsm', SINGLE_BLOB) rs 

..和,如數據庫和文件上傳都在同一臺機器上,這種運行幾乎瞬間。

正如我所說的,總的來說,它需要3-4秒將文件複製到SQL Server計算機上的文件夾,並運行此存儲過程,相比之下,使用C#代碼與LINQ執行相同的操作需要40-50秒或EF。

導出從SQL Server blob數據到外部文件

而且,當然,同樣是在相反的方向實現。首先,我寫了一些C#/ LINQ代碼來加載一個(7Mb!)數據庫記錄並將其二進制數據寫入一個原始文件。這需要大約30-40秒的時間來運行。

但如果我導出SQL Server數據到一個文件(保存在SQL Server計算機上)第一..

EXEC master..xp_cmdshell 'BCP "select ef.ExtFile_Data FROM [External_File] ef where ExtFile_ID = 585" queryout "D:\ImportData\SomeExcelFile.xslx" -T -N' 

...然後從SQL Server文件夾中的文件複製到用戶的文件夾,然後再一次,它在幾秒鐘內運行。

這是我的問題:爲什麼LINQ和EF在將大量記錄插入數據庫時​​如此糟糕?

我假設潛伏期(我們之間的距離,這裏是歐洲,還有我們在美國的數據中心)是導致延遲的主要原因,但是一個慢速標準文件複製可能會更快。

我錯過了什麼嗎?

很顯然,我發現walkarounds到這些問題,但它們涉及加在SQL Server計算機一些額外的權限,我們的SQL Server的機器和共享文件夾,我們的數據庫管理員真的不喜歡像「xp_cmdshell授予的東西的權利」 ......

幾個月後...

我這周再次有同樣的問題,並試圖凱文·H公司建議,使用大容量插入插入大(6MB)記錄到SQL服務器。

使用批量插入,即使我們的數據中心距離6000英里,插入6Mb記錄也需要大約90秒。因此,故事的寓意:當插入非常大的數據庫記錄時,避免使用常規的SubmitChanges()命令,並堅持使用批量插入。

+6

不要將7mb文件插入到SQL服務器中...... –

+1

嗯......你正在上傳7MB到遠程服務器。這可能是你的問題。 – FreeAsInBeer

+0

是的,我知道包含7Mb數據的記錄遠非理想......但我的觀點是,SQL Server爲這種確切場景提供了blob數據類型,LINQ/EF很高興支持這種類型,但它們的性能很差。 。我的問題是:*爲什麼*表現如此糟糕..?爲什麼編寫單個記錄要比將文件複製到服務器要長20倍?爲什麼使用LINQ/EF會出現延遲這樣的問題? –

回答

5

您可以嘗試使用Profiler來查看實體框架在插入時執行的操作。例如,如果它從表格中選擇數據,則可能需要很長時間才能通過線路返回數據,並且您可能不會在本地注意到這一點。

我發現從c#中將大量數據(記錄數和記錄大小)加載到sql server的最佳方式是使用SqlBulkCopy類。即使您只插入1條記錄,您仍然可以從此更改中受益。

要使用批量複製,只需創建一個與表格結構相匹配的數據表即可。然後像這樣調用代碼。

using (SqlConnection destinationConnection = new SqlConnection(connectionString)) 
using (SqlBulkCopy bulkCopy = new SqlBulkCopy(destinationConnection)) 
{ 
    bulkCopy.DestinationTableName = "External_File"; 
    bulkCopy.WriteToServer(dataTable); 
} 
+0

該死的。你是絕對正確的。具有諷刺意味的是,在過去,我已經記錄瞭如何做到批量插入*批量*行,但是我沒有意識到這對於插入巨大的記錄也是一樣有用。偉大的小費,謝謝! http://www.mikesknowledgebase.com/pages/LINQ/InsertAndDeletes.htm –