从 C# 调用的 Azure SQL 存储过程慢得离谱

LMS*_*LMS 10 c# sql performance stored-procedures azure

总结

我们有两个相同的数据库,一个在本地服务器上,一个在 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'
Run Code Online (Sandbox Code Playgroud)

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

连接到 Azure 上的数据库:-

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

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

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

SET ARITHABORT ON; 
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

仍然没有快乐,所以我得到了两者的执行计划详细信息:-

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')
Run Code Online (Sandbox Code Playgroud)

只是为了检查,我在 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
Run Code Online (Sandbox Code Playgroud)

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

因此,检查执行计划的设置

SELECT * FROM sys.dm_exec_plan_attributes (0x05002D00D1A1EA5510E66E783602000001);
SELECT * FROM sys.dm_exec_plan_attributes (0x05002D00D1A1EA55E0FC6E783602000001);
Run Code Online (Sandbox Code Playgroud)

比较设置

两者的 Set_Options 都是4345,所以他们肯定都在使用 ARITHABORT。

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

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

因此,我调用了有关执行计划的信息:-

SELECT * FROM sys.dm_exec_query_plan (0x05002D00D1A1EA5510E66E783602000001);
SELECT * FROM sys.dm_exec_query_plan (0x05002D00D1A1EA55E0FC6E783602000001);
Run Code Online (Sandbox Code Playgroud)

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

比较执行计划

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

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

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

比较执行计划

编译时间和 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>
Run Code Online (Sandbox Code Playgroud)

编辑:一些进展

我尝试了从谷歌搜索中得到的几件事:-

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 + ';'
Run Code Online (Sandbox Code Playgroud)

这产生了一组 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;
Run Code Online (Sandbox Code Playgroud)

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

'DISABLE_DEF_CNST_CHK' is not a recognized SET option.
Run Code Online (Sandbox Code Playgroud)

将此添加到存储过程的开头将时间从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 + ';'
Run Code Online (Sandbox Code Playgroud)

这返回

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;
Run Code Online (Sandbox Code Playgroud)

再次,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
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

将此设置为关闭后,分别尝试了两次 SSMS 和 C#。

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

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

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

ALTER DATABASE SCOPED CONFIGURATION SET PARAMETER_SNIFFING = OFF;
Run Code Online (Sandbox Code Playgroud)

还是 15+ 分钟。

嗯,值得一试!

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

编辑 #4:Azure 临时池配置和自动调整

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

计时测试

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

然后我将其发布在 Azure MSDN 站点上(当他们最终开始验证我的帐户时),他们建议浏览 Azure 门户上的所有性能选项,看看是否有任何推荐。

Azure 性能选项

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

然后我将自动调整从“服务器”翻转到“Azure 默认值”

Azure 默认值

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

之后的计时测试

过去需要 17 分钟的查询现在通常只需要 13 秒,这是一个巨大的改进!好极了!

其余的则是喜忧参半。C 通常更快,大多数仍然花费大约相同的时间,而 E 现在花费几乎两倍的时间(从 14 秒增加了 26 秒)。

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

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

l33*_*33t -4

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

[YourDatabaseName].[dbo].[usp_DevelopmentSearch_Select]

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

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

  • 事实并非如此。对于初学者来说,每个查询都是针对连接的数据库的。它是经常被省略的*模式*,导致在所有模式中进行搜索。本地数据库服务器拥有比托管数据库“多得多”的数据库,从设计上来说,托管数据库会阻止访问其他人的数据库。简单的搜索也永远不会将 1 秒的查询转换为 17 分钟的查询。 (2认同)