Azure DB 上 C# 中的 SQL 插入语句 - 运行速度非常慢

Posted

技术标签:

【中文标题】Azure DB 上 C# 中的 SQL 插入语句 - 运行速度非常慢【英文标题】:SQL Insert statements in C# on Azure DB - Running very slow 【发布时间】:2021-10-31 07:08:07 【问题描述】:

我正在我们的网络应用程序中开发一个导入器。使用我目前拥有的代码,当您通过本地 SQL 服务器连接时,它运行良好且在合理范围内。我还在创建一个.sql 脚本,他们也可以下载它

示例 1

4 万条记录,8 列,从 1 分 30 秒到 2 分钟

当我将其移至生产和 Azure 应用服务时,它的运行速度非常慢。

示例 2

40k 条记录,8 列,从 15 分钟到 18 分钟

当前数据库设置为:定价层:标准 S2:50 个 DTU

代码如下:

using (var sqlConnection = new SqlConnection(connectionString))

    try
    
        var generatedScriptFilePathInfo = GetImportGeneratedScriptFilePath(trackingInfo.UploadTempDirectoryPath, trackingInfo.FileDetail);

        using (FileStream fileStream = File.Create(generatedScriptFilePathInfo.GeneratedScriptFilePath))
        
            using (StreamWriter writer = new StreamWriter(fileStream))
            
                sqlConnection.Open();
                sqlTransaction = sqlConnection.BeginTransaction();
                        
                await writer.WriteLineAsync("/* Insert Scripts */").ConfigureAwait(false);

                foreach (var item in trackingInfo.InsertSqlScript)
                
                    errorSqlScript = item;

                    using (var cmd = new SqlCommand(item, sqlConnection, sqlTransaction))
                    
                        cmd.CommandTimeout = 800;
                        cmd.CommandType = CommandType.Text;
                        await cmd.ExecuteScalarAsync().ConfigureAwait(false);
                    

                    currentRowLine++;
                    rowsProcessedUpdateEveryXCounter++;
                    rowsProcessedTotal++;

                    // append insert statement to the file
                    await writer.WriteLineAsync(item).ConfigureAwait(false);
                

                // write out a couple of blank lines to separate insert statements from post scripts (if there are any)
                await writer.WriteLineAsync(string.Empty).ConfigureAwait(false);
                await writer.WriteLineAsync(string.Empty).ConfigureAwait(false);
            
        
    
    catch (OverflowException exOverFlow)
    
        sqlTransaction.Rollback();
        sqlTransaction.Dispose();
        trackingInfo.IsSuccessful = false;
        trackingInfo.ImportMetricUpdateError = new ImportMetricUpdateErrorDTO(trackingInfo.ImportMetricId)
                
                    ErrorLineNbr = currentRowLine + 1, // add one to go ahead and count the record we are on to sync up with the file
                    ErrorMessage = string.Format(CultureInfo.CurrentCulture, "0", ImporterHelper.ArithmeticOperationOverflowFriendlyErrorText),
                    ErrorSQL = errorSqlScript,
                    RowsProcessed = currentRowLine
                ;

        await LogImporterError(trackingInfo.FileDetail, exOverFlow.ToString(), currentUserId).ConfigureAwait(false);
        await UpdateImportAfterFailure(trackingInfo.ImportMetricId, exOverFlow.Message, currentUserId).ConfigureAwait(false);

        return trackingInfo;
    
    catch (Exception ex)
    
        sqlTransaction.Rollback();
        sqlTransaction.Dispose();

        trackingInfo.IsSuccessful = false;
        trackingInfo.ImportMetricUpdateError = new ImportMetricUpdateErrorDTO(trackingInfo.ImportMetricId)
                
                    ErrorLineNbr = currentRowLine + 1, // add one to go ahead and count the record we are on to sync up with the file
                    ErrorMessage = string.Format(CultureInfo.CurrentCulture, "0", ex.Message),
                    ErrorSQL = errorSqlScript,
                    RowsProcessed = currentRowLine
                ;

        await LogImporterError(trackingInfo.FileDetail, ex.ToString(), currentUserId).ConfigureAwait(false);
        await UpdateImportAfterFailure(trackingInfo.ImportMetricId, ex.Message, currentUserId).ConfigureAwait(false);

        return trackingInfo;
    

问题

    有没有办法在 Azure 上加快这个速度?或者是升级DTUs的唯一方法? 我们也在研究 SQL 批量复制。这是否会帮助或仍然会导致 Azure 运行缓慢:https://docs.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlbulkcopy?redirectedfrom=MSDN&view=dotnet-plat-ext-5.0

期望的结果

在本地 SQL Server 数据库中运行时以相同的速度运行

【问题讨论】:

记住数据库离你很远......所以你必须做一些事情来减少往返......批量操作是一种方法 @Ctznkane525 击败了我,但您的方法似乎与您发布的时间一致 - 15 分钟内有 4 万条记录,即 90 万毫秒,每次往返大约 22.5 毫秒。在本地,根据您发布的时间(或每次往返 2.25 毫秒),您运行相同工作负载的速度大约快了 10 倍,这是有道理的。您可以从 Azure VM(Azure SQL DB 本地)运行它并查看结果是否更接近您的本地测试? SqlBulkCopy 将大大加快速度。而且您的代码无论如何都有问题:缺少参数化,并且在事务对象上缺少using 如果不是发送单独的插入,而是构建一个包含所有插入的命令并在数据库中只运行一次,会发生什么? 您正在执行 RBAR 插入,这是将数据放入数据库的最慢方法。将数据发送到服务器一次,分批处理,性能提升一个数量级。 【参考方案1】:

现在,我更新了我的代码以根据记录的数量对插入语句进行批处理。如果记录数超过 10k,那么它将通过将总数除以 10 来对它们进行批处理。

这有助于在我们的 Azure 实例上实现 BIG TIME。我能够在 30 秒内添加 40k 条记录。我还认为其中一些问题是有多少不同的插槽在 Azure 上使用我们的应用服务。

由于用户需要导入更大的 excel 文件,我们稍后也可能会转移到 SQLBulkCopy

感谢大家的帮助和见解!

        // apply the create table SQL script if found.
        if (string.IsNullOrWhiteSpace(trackingInfo.InsertSqlScript.ToString()) == false)
        
            int? updateEveryXRecords = GetProcessedEveryXTimesForApplyingInsertStatementsValue(trackingInfo.FileDetail);
            trackingInfo.FileDetail = UpdateImportMetricStatus(trackingInfo.FileDetail, ImportMetricStatus.ApplyingInsertScripts, currentUserId);

            int rowsProcessedUpdateEveryXCounter = 0;
            int rowsProcessedTotal = 0;

            await UpdateImportMetricsRowsProcessed(trackingInfo.ImportMetricId, rowsProcessedTotal, trackingInfo.FileDetail.ImportMetricStatusHistories).ConfigureAwait(false);

            bool isBulkMode = trackingInfo.InsertSqlScript.Count >= 10000;

            await writer.WriteLineAsync("/* Insert Scripts */").ConfigureAwait(false);
            int insertCounter = 0;
            int bulkCounter = 0;
            int bulkProcessingAmount = 0;
            int lastInsertCounter = 0;
            if (isBulkMode == true)
            
                bulkProcessingAmount = trackingInfo.InsertSqlScript.Count / 10;
            

            await LogInsertBulkStatus(trackingInfo.FileDetail, isBulkMode, trackingInfo.InsertSqlScript.Count, bulkProcessingAmount, currentUserId).ConfigureAwait(false);

            StringBuilder sbInsertBulk = new StringBuilder();
            foreach (var item in trackingInfo.InsertSqlScript)
            
                if (isBulkMode == false)
                
                    errorSqlScript = item;

                    using (var cmd = new SqlCommand(item, sqlConnection, sqlTransaction))
                    
                        cmd.CommandTimeout = 800;
                        cmd.CommandType = CommandType.Text;
                        await cmd.ExecuteScalarAsync().ConfigureAwait(false);
                    

                    currentRowLine++;
                    rowsProcessedUpdateEveryXCounter++;
                    rowsProcessedTotal++;

                    // append insert statement to the file
                    await writer.WriteLineAsync(item).ConfigureAwait(false);

                    // Update database with the insert statement created count to alert the user of the status.
                    if (updateEveryXRecords.HasValue)
                    
                        if (updateEveryXRecords.Value == rowsProcessedUpdateEveryXCounter)
                        
                            await UpdateImportMetricsRowsProcessed(trackingInfo.ImportMetricId, rowsProcessedTotal, trackingInfo.FileDetail.ImportMetricStatusHistories).ConfigureAwait(false);
                            rowsProcessedUpdateEveryXCounter = 0;
                        
                    
                
                else
                
                    sbInsertBulk.AppendLine(item);

                    if (bulkCounter < bulkProcessingAmount)
                    
                        errorSqlScript = string.Format(CultureInfo.CurrentCulture, "IsBulkMode is True | insertCounter = 0", insertCounter);

                        bulkCounter++;
                    
                    else
                    
                        // display to the end user
                        errorSqlScript = string.Format(CultureInfo.CurrentCulture, "IsBulkMode is True | currentInsertCounter value = 0 | lastInsertCounter (insertCounter when the last bulk insert occurred): 1", insertCounter, lastInsertCounter);

                        await ApplyBulkInsertStatements(sbInsertBulk, writer, sqlConnection, sqlTransaction, trackingInfo, rowsProcessedTotal).ConfigureAwait(false);

                        bulkCounter = 0;
                        sbInsertBulk.Clear();
                        lastInsertCounter = insertCounter;
                    

                    rowsProcessedTotal++;
                

                insertCounter++;
            

            // get the remaining records after finishing the forEach insert statement
            if (isBulkMode == true)
            
                await ApplyBulkInsertStatements(sbInsertBulk, writer, sqlConnection, sqlTransaction, trackingInfo, rowsProcessedTotal).ConfigureAwait(false);
            
        

        /// <summary>
        /// Applies the bulk insert statements.
        /// </summary>
        /// <param name="sbInsertBulk">The sb insert bulk.</param>
        /// <param name="wrtier">The wrtier.</param>
        /// <param name="sqlConnection">The SQL connection.</param>
        /// <param name="sqlTransaction">The SQL transaction.</param>
        /// <param name="trackingInfo">The tracking information.</param>
        /// <param name="rowsProcessedTotal">The rows processed total.</param>
        /// <returns>Task</returns>
        private async Task ApplyBulkInsertStatements(
                                                     StringBuilder sbInsertBulk,
                                                     StreamWriter wrtier,
                                                     SqlConnection sqlConnection,
                                                     SqlTransaction sqlTransaction,
                                                     ProcessImporterTrackingDTO trackingInfo,
                                                     int rowsProcessedTotal)
        
            var bulkInsertStatements = sbInsertBulk.ToString();
            using (var cmd = new SqlCommand(bulkInsertStatements, sqlConnection, sqlTransaction))
            
                cmd.CommandTimeout = 800;
                cmd.CommandType = CommandType.Text;
                await cmd.ExecuteScalarAsync().ConfigureAwait(false);
            

            // append insert statement to the file
            await wrtier.WriteLineAsync(bulkInsertStatements).ConfigureAwait(false);

            // Update database with the insert statement created count to alert the user of the status.
            await UpdateImportMetricsRowsProcessed(trackingInfo.ImportMetricId, rowsProcessedTotal, trackingInfo.FileDetail.ImportMetricStatusHistories).ConfigureAwait(false);
        

【讨论】:

以上是关于Azure DB 上 C# 中的 SQL 插入语句 - 运行速度非常慢的主要内容,如果未能解决你的问题,请参考以下文章

来自 C# 的 Azure SQL DB 连接问题

c#中往mysql里批量插入上万条数据,有比较高效的方法吗

C# 使用OleDB写数据到excel的插入数据语句!!!

嗨,我们可以使用自动电源将大 XML 文件存储到 azure sql db 中吗?

c#用 ef怎么执行sql语句

C# 中的 Sql Parser 用于语法检查 Oracle 语句