Estou tendo grandes problemas de desempenho de SQL ao usar chamadas assíncronas. Criei um pequeno caso para demonstrar o problema.
Eu criei um banco de dados em um SQL Server 2016 que reside em nossa LAN (portanto, não um localDB).
Nesse banco de dados, tenho uma tabela WorkingCopy
com 2 colunas:
Id (nvarchar(255, PK))
Value (nvarchar(max))
DDL
CREATE TABLE [dbo].[Workingcopy]
(
[Id] [nvarchar](255) NOT NULL,
[Value] [nvarchar](max) NULL,
CONSTRAINT [PK_Workingcopy]
PRIMARY KEY CLUSTERED ([Id] ASC)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
Nessa tabela, inseri um único registro ( id
= 'PerfUnitTest', Value
é uma string de 1,5 MB (um zip de um conjunto de dados JSON maior)).
Agora, se eu executar a consulta no SSMS:
SELECT [Value]
FROM [Workingcopy]
WHERE id = 'perfunittest'
Eu imediatamente obtenho o resultado e vejo no SQL Servre Profiler que o tempo de execução foi em torno de 20 milissegundos. Tudo normal.
Ao executar a consulta do código .NET (4.6) usando um simples SqlConnection
:
// at this point, the connection is already open
var command = new SqlCommand($"SELECT Value FROM WorkingCopy WHERE Id = @Id", _connection);
command.Parameters.Add("@Id", SqlDbType.NVarChar, 255).Value = key;
string value = command.ExecuteScalar() as string;
O tempo de execução para isso também é cerca de 20-30 milissegundos.
Mas ao alterá-lo para o código assíncrono:
string value = await command.ExecuteScalarAsync() as string;
O tempo de execução é de repente 1800 ms ! Também no SQL Server Profiler, vejo que a duração da execução da consulta é de mais de um segundo. Embora a consulta executada relatada pelo criador de perfil seja exatamente a mesma que a versão não Async.
Mas fica pior. Se eu brincar com o tamanho do pacote na string de conexão, obtenho os seguintes resultados:
Tamanho do pacote 32768: [TIMING]: ExecuteScalarAsync em SqlValueStore -> tempo decorrido: 450 ms
Tamanho do pacote 4096: [TIMING]: ExecuteScalarAsync em SqlValueStore -> tempo decorrido: 3667 ms
Tamanho do pacote 512: [TIMING]: ExecuteScalarAsync em SqlValueStore -> tempo decorrido: 30776 ms
30.000 ms !! Isso é mais de 1000 vezes mais lento do que a versão não assíncrona. E o SQL Server Profiler relata que a execução da consulta levou mais de 10 segundos. Isso nem mesmo explica para onde foram os outros 20 segundos!
Então eu mudei de volta para a versão sincronizada e também brinquei com o tamanho do pacote, e embora tenha impactado um pouco o tempo de execução, não foi tão dramático quanto na versão assíncrona.
Como nota lateral, se colocar apenas uma pequena string (<100 bytes) no valor, a execução da consulta assíncrona será tão rápida quanto a versão de sincronização (resultado em 1 ou 2 ms).
Estou realmente perplexo com isso, especialmente porque estou usando o integrado SqlConnection
, nem mesmo um ORM. Além disso, ao pesquisar por aí, não encontrei nada que pudesse explicar esse comportamento. Alguma ideia?
GetSqlChars
ou GetSqlBinary
recupere-os em um modo de streaming. Considere também armazená-los como dados FILESTREAM - não há razão para salvar 1,5 MB de dados na página de dados de uma tabela