【问题标题】:Azure SQL stored procedure ridiculously slow called from C#从 C# 调用的 Azure SQL 存储过程异常缓慢
【发布时间】:2020-03-21 00:14:15
【问题描述】:

总结

我们有两个相同的数据库,一个在本地服务器上,一个在 Azure 上。

我们有一个 C# 系统可以访问这些数据库,调用存储过程。

当从 C# 系统调用到 Azure 数据库时,存储过程运行得非常非常缓慢。它们从 C# 到本地服务器以及从 SSMS 到 Azure 和本地数据库都运行良好。

例如,调用存储过程'usp_DevelopmentSearch_Select'

本地数据库,SSMS:1 秒

本地数据库,C#:1 秒

Azure 数据库,SSMS:1 秒

Azure 数据库,C#:17 分钟

这发生在多个存储过程上,我只是以 usp_DevelopmentSearch_Select 为例,来测试解决方案并跟踪执行计划。

我已经排除了 ARITHABORT(通常的嫌疑人),似乎在 SSMS 和 C# 系统中运行 usp_DevelopmentSearch_Select 会生成功能相同的执行计划。

详情

我们编写了一个非常大的 C# 系统,它访问 SQL Server 数据库。

目前,我们所有的客户都在自己的服务器上本地托管自己的数据库,但我们正在研究在 Azure 上托管数据库的选项。因此,我建立了一些小型 Azure 测试数据库,解决了问题,并让 Azure 托管的系统运行起来。

然后我复制了我们客户的一个数据库,以比较本地托管与 Azure 上托管的性能。

实际的客户端数据库在 Azure 上表现不佳!

第一个屏幕调用存储过程“usp_DevelopmentSearch_Select”

连接到他们服务器上的数据库:-

在 SSMS 中,调用存储过程(如下)在大约 1 秒内返回值

EXEC usp_DevelopmentSearch_Select @MaxRecord = 100, @SearchType = 'CUR'

在我们的 C# 程序中,调用存储过程会在大约 1 秒内返回值

连接到 Azure 上的数据库:-

在SSMS中,调用存储过程大约1秒后返回值

在我们的 C# 程序中,调用存储过程会在大约 17 分钟内返回值!

在 SSMS 中快而在 C# 中慢通常意味着 ARITHABORT,所以我在存储过程开始时将其打开:

SET ARITHABORT ON; 

这并没有什么区别,所以我更新了它以将传递的参数转换为局部变量。

ALTER PROCEDURE [dbo].[usp_DevelopmentSearch_Select]
     (@MAXRECORD INT,
      @SEARCHTYPE VARCHAR(3))
AS
BEGIN
    SET ARITHABORT ON; 

    DECLARE @MAXRECORD_Var INT = @MAXRECORD
    DECLARE @SEARCHTYPE_Var VARCHAR(3) = @SEARCHTYPE

    ... (Updated all references to @MAXRECORD and @SEARCHTYPE to @MAXRECORD_Var and @SEARCHTYPE_Var)

END

仍然不高兴,所以我得到了两者的执行计划详细信息:-

select o.object_id, s.plan_handle, h.query_plan 
from sys.objects o 
inner join sys.dm_exec_procedure_stats s on o.object_id = s.object_id
cross apply sys.dm_exec_query_plan(s.plan_handle) h
where o.object_id = object_id('usp_DevelopmentSearch_Select')

为了检查,我在 C# 程序中重新加载了屏幕,并检查了正在运行的查询:-

SELECT sqltext.TEXT,
req.session_id,
req.status,
req.command,
req.cpu_time,
req.total_elapsed_time,
req.plan_handle
FROM sys.dm_exec_requests req
CROSS APPLY sys.dm_exec_sql_text(sql_handle) AS sqltext

肯定是使用上面返回的两个执行计划之一。

所以,请检查执行计划的设置

SELECT * FROM sys.dm_exec_plan_attributes (0x05002D00D1A1EA5510E66E783602000001);
SELECT * FROM sys.dm_exec_plan_attributes (0x05002D00D1A1EA55E0FC6E783602000001);

Set_Options 对于两者来说都是 4345,所以他们肯定都在使用 ARITHABORT。

唯一的区别是本地化位:语言和日期格式。 Azure 数据库卡在美国,似乎无法改变这一点,而 C# 程序将其强制为英国。

我尝试了 C# 程序,但没有将其强制为 British,但仍然遇到同样的问题。它还使用了完全相同的执行计划,因此显然本地化不会影响这一点。

所以,我调出了关于执行计划的信息:-

SELECT * FROM sys.dm_exec_query_plan (0x05002D00D1A1EA5510E66E783602000001);
SELECT * FROM sys.dm_exec_query_plan (0x05002D00D1A1EA55E0FC6E783602000001);

把它们都保存了,比较结果:-

最左边的两列显示了整体比较:黄色不同,白色相同。 如您所见,这两个执行计划几乎相同,只是顶部有一些差异。

在上面的屏幕截图中可以看到第一个区别:SSMS(左)窗格中的“StatementCompId”比 C#(右)窗格中的高一个。 Google 不想告诉我 StatementCompId 是什么,但鉴于它们是按顺序排列的,我猜这是执行它们的顺序,而 SSMS 更高,因为调用的 EXEC 命令SP 算作一。

为方便起见,我已将所有剩余的差异汇总到一个屏幕截图中:-

编译时间和 CPU 使用率、可用内存以及更多“StatementCompId”

因此,这两个执行计划在功能上是相同的,具有相同的设置(除了似乎没有效果的本地化)。

那么为什么从 C# 调用 Azure SP 大约需要 17 分钟,而从 SSMS 调用 Azure SP 或从本地托管数据库调用本地 SP 大约需要 1 秒?

存储过程本身只是一个 SELECT FROM,有几个 LEFT JOIN 到其他表,没什么特别的,它在本地托管的数据库上从来没有给我们带来任何麻烦。

SELECT TOP (@MAXRECORD_Var) <FieldList>
FROM (
    SELECT DISTINCT <FieldList>
    FROM <TableName> WITH (NOLOCK)
    LEFT JOIN <TableName> WITH (NOLOCK) ON <Link>
    LEFT JOIN <TableName> WITH (NOLOCK) ON <Link>
    LEFT JOIN <TableName> WITH (NOLOCK) ON <Link>
    LEFT JOIN <TableName> WITH (NOLOCK) ON <Link>
    LEFT JOIN <TableName> WITH (NOLOCK) ON <Link>
    WHERE (
        <Conditions>
    ) AS Base
ORDER BY <FieldName>

编辑:一些进展

我尝试了一些从谷歌上搜索出来的东西:-

1) 重新编译

我尝试将它添加到存储过程中,没有任何区别

2) 选项(优化 (@MAXRECORD_Var UNKNOWN, @SEARCHTYPE_Var UNKNOWN))

我尝试将它添加到存储过程中,没有任何区别

3) 显式设置所有选项

这一点产生了显着的差异(但仍然太小)!

我写了一个查询来告诉我当前的选项

DECLARE @options INT
SELECT @options = @@OPTIONS
PRINT @options
PRINT 'SET DISABLE_DEF_CNST_CHK ' + CASE WHEN ( (1 & @options) = 1 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET IMPLICIT_TRANSACTIONS ' + CASE WHEN ( (2 & @options) = 2 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET CURSOR_CLOSE_ON_COMMIT ' + CASE WHEN ( (4 & @options) = 4 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_WARNINGS ' + CASE WHEN ( (8 & @options) = 8 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_PADDING ' + CASE WHEN ( (16 & @options) = 16 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_NULLS ' + CASE WHEN ( (32 & @options) = 32 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ARITHABORT ' + CASE WHEN ( (64 & @options) = 64 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ARITHIGNORE ' + CASE WHEN ( (128 & @options) = 128 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET QUOTED_IDENTIFIER ' + CASE WHEN ( (256 & @options) = 256 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET NOCOUNT ' + CASE WHEN ( (512 & @options) = 512 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_NULL_DFLT_ON ' + CASE WHEN ( (1024 & @options) = 1024 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_NULL_DFLT_OFF ' + CASE WHEN ( (2048 & @options) = 2048 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET CONCAT_NULL_YIELDS_NULL ' + CASE WHEN ( (4096 & @options) = 4096 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET NUMERIC_ROUNDABORT ' + CASE WHEN ( (8192 & @options) = 8192 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET XACT_ABORT ' + CASE WHEN ( (16384 & @options) = 16384 ) THEN 'ON' ELSE 'OFF' END + ';'

这产生了一组 SET 语句,以及当前的 Options 值

5496
SET DISABLE_DEF_CNST_CHK OFF;
SET IMPLICIT_TRANSACTIONS OFF;
SET CURSOR_CLOSE_ON_COMMIT OFF;
SET ANSI_WARNINGS ON;
SET ANSI_PADDING ON;
SET ANSI_NULLS ON;
SET ARITHABORT ON;
SET ARITHIGNORE OFF;
SET QUOTED_IDENTIFIER ON;
SET NOCOUNT OFF;
SET ANSI_NULL_DFLT_ON ON;
SET ANSI_NULL_DFLT_OFF OFF;
SET CONCAT_NULL_YIELDS_NULL ON;
SET NUMERIC_ROUNDABORT OFF;
SET XACT_ABORT OFF;

注意:运行 SET DISABLE_DEF_CNST_CHK OFF;抛出一个错误,所以我把那个注释掉了。

'DISABLE_DEF_CNST_CHK' is not a recognized SET option.

将此添加到存储过程的开头使时间从 17 分钟 减少到 40 秒

在 SSMS 中运行仍然远远超过 1 秒,仍然不足以使用,但进步不小。

但是,我注意到它返回的 Options 值 (5496) 与我从上面的执行计划详细信息中得到的值 (4345) 不同,而且还有一些与该数据库的设置不同的设置。

所以,我重新运行了硬编码为 4345 的查询

DECLARE @options INT
SELECT @options = 4345 --@@OPTIONS
PRINT @options
PRINT 'SET DISABLE_DEF_CNST_CHK ' + CASE WHEN ( (1 & @options) = 1 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET IMPLICIT_TRANSACTIONS ' + CASE WHEN ( (2 & @options) = 2 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET CURSOR_CLOSE_ON_COMMIT ' + CASE WHEN ( (4 & @options) = 4 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_WARNINGS ' + CASE WHEN ( (8 & @options) = 8 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_PADDING ' + CASE WHEN ( (16 & @options) = 16 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_NULLS ' + CASE WHEN ( (32 & @options) = 32 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ARITHABORT ' + CASE WHEN ( (64 & @options) = 64 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ARITHIGNORE ' + CASE WHEN ( (128 & @options) = 128 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET QUOTED_IDENTIFIER ' + CASE WHEN ( (256 & @options) = 256 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET NOCOUNT ' + CASE WHEN ( (512 & @options) = 512 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_NULL_DFLT_ON ' + CASE WHEN ( (1024 & @options) = 1024 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET ANSI_NULL_DFLT_OFF ' + CASE WHEN ( (2048 & @options) = 2048 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET CONCAT_NULL_YIELDS_NULL ' + CASE WHEN ( (4096 & @options) = 4096 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET NUMERIC_ROUNDABORT ' + CASE WHEN ( (8192 & @options) = 8192 ) THEN 'ON' ELSE 'OFF' END + ';'
PRINT 'SET XACT_ABORT ' + CASE WHEN ( (16384 & @options) = 16384 ) THEN 'ON' ELSE 'OFF' END + ';'

返回

4345
SET DISABLE_DEF_CNST_CHK ON;
SET IMPLICIT_TRANSACTIONS OFF;
SET CURSOR_CLOSE_ON_COMMIT OFF;
SET ANSI_WARNINGS ON;
SET ANSI_PADDING ON;
SET ANSI_NULLS ON;
SET ARITHABORT ON;
SET ARITHIGNORE ON;
SET QUOTED_IDENTIFIER OFF;
SET NOCOUNT OFF;
SET ANSI_NULL_DFLT_ON OFF;
SET ANSI_NULL_DFLT_OFF OFF;
SET CONCAT_NULL_YIELDS_NULL ON;
SET NUMERIC_ROUNDABORT OFF;
SET XACT_ABORT OFF;

再次,SET DISABLE_DEF_CNST_CHK ON; 行表示这不是您可以设置的选项,因此我将其注释掉。

使用这些 SET 值更新存储过程,然后重试。

仍然需要 40 秒,所以没有进一步的进展。

在 SSMS 中运行它仍然需要 1 秒,所以至少它没有破坏它,并不是说它有任何帮助,但很高兴知道!

编辑#2:或者不...

似乎昨天的明显进展只是昙花一现:又回到了 17 分钟! (没有任何改变)

尝试结合所有三个选项:WITH RECOMPILE、OPTION OPTIMIZE 并显式设置 SET OPTIONS。仍然需要 17 分钟。

编辑3参数嗅探设置

在 SQL Azure 中,您可以从数据库选项屏幕关闭参数嗅探。

并使用检查它们

SELECT * FROM sys.database_scoped_configurations

将其设置为 OFF 后,分别尝试了 SSMS 和 C# 两次。

和以前一样,SSMS 需要 1 秒,C# 仍然需要 15 分钟以上。

当然,鉴于 C# 在连接时强制加载参数到特定状态,它完全有可能覆盖它。

所以,只是说我试过了,我在存储过程中添加了关闭它

ALTER DATABASE SCOPED CONFIGURATION SET PARAMETER_SNIFFING = OFF;

还有 15 分钟以上。

嗯,值得一试!

此外,还有许多新参数可供查找和测试。

编辑 #4:Azure 暂存池配置和自动调整

我在暂存池上尝试了几种不同的配置,看看是否有所不同。我没有尝试最糟糕的查询,因为增加 eDTU 会花费我们很多钱,但我尝试了其他几个,每次两次(每次都在列表中,所以不是同一个直接两次)。

从 50 个 eDTU 到 100 个 eDTU 会有所不同,所以我猜在我们的测试弹性池中我们使用了全部 50 个,但之后就没有任何区别了。奇怪的是,高级版在某些地方的表现比标准版差。

然后我在 Azure MSDN 网站上发布了这个(当他们终于开始验证我的帐户时),他们建议检查 Azure 门户上的所有性能选项,看看是否有什么建议。

它建议了几个索引,我启用了它们,但仅此而已。

然后我将 Automatic Tuning 从“Server”切换到“Azure Defaults”

我重新运行了大部分相同的计时测试,只是想看看它有什么不同。

以前需要 17 分钟的查询现在通常需要 13 秒,这是一个巨大的改进!耶!

其余的情况好坏参半。 C 通常更快,大部分时间仍然差不多,E 现在几乎是两倍(从 14 秒增加到 26 秒)。

虽然更改 eDTU 大小可能会重置调整,但结果似乎也比以前有更多的差异。第二次运行通常比第一次好,而且通常很明显。

仍然比在本地服务器上针对数据库运行相同系统慢很多,但至少对于最慢的存储过程来说是一个巨大的改进。

【问题讨论】:

  • 我不确定dba.stackexchange.com 是否适合回答这个问题...
  • NOLOCK 是一个错误。这并不意味着donb't take locks。这意味着read dirty data while taking excessive locks。您尝试使用该提示的事实意味着您已经遇到了性能问题
  • 您没有发布查询或执行计划,因此无法猜测发生了什么。唯一可以确定的是该代码中有一些错误的查询。 SSMS 和您的程序(不是 C#,您的程序)将加载不同数量的数据,对 ARITH_ABORT 使用不同的设置等。不过,您再次要求人们猜测。差异是否是因为 SSMS 不会 加载所有内容?也许,也许不是。是因为 Azure SQL 实例太小吗?也许,所有这些 NOLOCK 都暗示了现有的性能问题。
  • 另一个潜在问题 - @SEARCHTYPE 是否意味着相同的存储过程执行不同的查询?与包罗万象的查询一样,问题可能在于第一次执行生成的执行计划不适用于后续调用。
  • 您已经竭尽全力地尝试了所有选项来解决参数嗅探问题....但我仍然认为这是一个参数嗅探问题。 “厨房水槽:存储过程很容易受到它们的影响。如果硬编码参数值会发生什么?还要检查服务器设置。也许一个人的 MAXDOP 设置不同。

标签: c# sql performance azure stored-procedures


【解决方案1】:

当您从C# 调用您的 SP 时,您应该包含数据库的名称:

[YourDatabaseName].[dbo].[usp_DevelopmentSearch_Select]

SSMS 中,您的数据库很可能处于活动状态。因此,服务器将知道您正在查询哪个数据库。在本地服务器上运行时,您很可能只有很少的数据库(也许只有一个?)。因此,您的本地服务器将知道您正在查询哪个数据库。

但是,在Azure 中,您很可能拥有多个数据库,因此它可能需要扫描多个数据库。这将解释您所看到的延迟。

【讨论】:

  • 它没有。对于初学者,每个查询都针对连接的数据库。 schema 经常被忽略,导致在所有模式中进行搜索。本地数据库服务器拥有比托管数据库更多的数据库,托管数据库在设计上会阻止访问其他人的数据库。简单的搜索也永远不会将 1 秒的查询转换为 17 分钟的查询。
【解决方案2】:

您是否排除了环境问题?检查您为 azure 服务器分配的内存、磁盘空间、cpu 和网络带宽。 SQL 占用内存和磁盘空间。

但是您可能已经检查了所有这些。我的下一个猜测是连接字符串。您肯定需要在连接字符串中使用不同的语法来连接到 Azure Sql 实例,而不是本地/网络托管的实例。有关 azure 特定连接字符串语法的详细信息,请参阅 connectionstrings.com。 (请记住,Azure Sql Server 与常规 Sql Server 是不同的软件。它们试图看起来相同并假装行为相同,但实际上它们实际上是不同的。)可能是身份验证或加密之类的东西是瓶颈。尤其是您的 SSMS 连接和代码连接字符串之间的身份验证可能会有所不同。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-18
    • 1970-01-01
    • 2023-03-09
    • 2011-09-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多