Com a configuração do banco de dados
CREATE TYPE dbo.TableType AS TABLE (
prop1 int,
prop2 datetime2,
prop3 varchar(1000)
);
GO
CREATE OR ALTER PROC dbo.TestTableTypePerf
@Data dbo.TableType READONLY
AS
SELECT COUNT(*)
FROM @Data;
O código a seguir passa os valores de TVP de forma streaming. O enumerável não é avaliado até depois da ExecuteScalarAsync
chamada e não há necessidade de que todos os 5.000.000 elementos sejam materializados em uma coleção no cliente.
Está se tornando cada vez mais popular evitar TVPs em favor de strings JSON. Algo semelhante pode ser feito para JSON?
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient.Server;
const string connectionString =
@"...";
await TvpTest();
return;
static async Task TvpTest()
{
await using var conn = new SqlConnection(connectionString);
await conn.OpenAsync();
await using var cmd = new SqlCommand("dbo.TestTableTypePerf", conn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter
{
ParameterName = "@Data",
SqlDbType = SqlDbType.Structured,
TypeName = "dbo.TableType",
Value = GetEnumerableOfRandomSqlDataRecords(5_000_000)
});
Console.WriteLine($"calling ExecuteScalarAsync at {DateTime.Now:O}");
var result = await cmd.ExecuteScalarAsync();
Console.WriteLine($"writing result at {DateTime.Now:O}");
Console.WriteLine(result);
}
static IEnumerable<SqlDataRecord> GetEnumerableOfRandomSqlDataRecords(uint length)
{
SqlMetaData[] metaData =
[
new SqlMetaData("prop1", SqlDbType.Int),
new SqlMetaData("prop2", SqlDbType.DateTime2),
new SqlMetaData("prop3", SqlDbType.VarChar, 1000)
];
foreach (var dto in GetEnumerableOfRandomDto(length))
{
var record = new SqlDataRecord(metaData);
record.SetInt32(0, dto.Prop1);
record.SetDateTime(1, dto.Prop2);
record.SetString(2, dto.Prop3);
yield return record;
}
}
static IEnumerable<Dto> GetEnumerableOfRandomDto(uint length)
{
var rnd = new Random();
for (var i = 0; i < length; i++)
{
yield return new Dto(rnd.Next(1, int.MaxValue),
DateTime.Now.AddMinutes(rnd.Next(1, 10000)),
Guid.NewGuid().ToString()
);
if ((i + 1) % 100_000 == 0)
Console.WriteLine($"Generated enumerable {i + 1} at {DateTime.Now:O}");
}
}
internal record Dto(int Prop1, DateTime Prop2, string Prop3);
Criei uma versão mais eficiente e simples do seu teste JSON e também o comparei com o teste TVP, usando o BenchmarkDotNet (o padrão ouro em benchmarking .NET).
Melhorias:
SELECT DISTINCT
sobre todas as colunas.prop2
eprop3
, para evitar sobrecargas deRandom
e interpolação de strings.TextReader
uso da novaSystem.IO.Pipelines
API da seguinte forma:Pipe
.Stream
e passe-o paraStreamReader
.Stream
Isso evita ter que usar toda essa complicação de passagem falsa , já que oPipe
faz tudo isso para nós.TvpTest
, você não deve recriarSqlDataRecord
. Ao contrário de qualquer outro uso deIEnumerable
no planeta, é recomendado reutilizar oSqlDataRecord
. Observe que aDateTime2
escala é explicitamente especificada.Os resultados do BenchmarkDotNet estão abaixo. Você pode ver que usar JSON é bem mais rápido para conjuntos de dados muito pequenos, mas os TVPs se distanciam massivamente na extremidade superior. Então parece melhor usar TVPs somente quando o conjunto de dados provavelmente atingirá 1000 linhas, devido a um custo inicial claramente maior.
Também testei com uma única
Prop1 int
coluna em ambos, com a consultaSELECT MAX(Prop1) FROM
para cada um. Os resultados são interessantes: a versão TVP agora usa muito menos memória, mesmo para 50000 linhas, mas JSON usou o mesmo, e a diferença de velocidade em contagens de linhas altas também foi maior.Outras notas:
StreamReader
tem a desvantagem de converter de UTF-8 para UTF-16. Eu vi uma pequena melhora em alguns casos usando avarbinary
e escrevendo o UTF-8Stream
diretamente no servidor, entãoCAST
ing server-side asvarchar
e então comonvarchar
, mas não foi consistente. Infelizmente, o SqlClient não aceita aStream
de bytes UTF-8 para umvarchar
valor.System.Text.Json
UTF-16 para escrever, eOPENJSON
ele só aceitanvarchar
, então é quase certo que haja alguma perda de eficiência na conversão..nodes
vários.values
métodos. Todos foram menos eficientes do que os outros dois métodos em todos os casos.XmlReader
precisa questring
valores sejam passados, o que significa que muitas strings são geradas para os valoresint
and convertidosDateTime
. Isso ainda pode ser uma opção para um caso de uso que envolva apenas strings.XmlSerializer
usará apenas umaXmlReader
saída.XDocument
pode gerar umXmlReader
, mas você ainda precisa gerar todas essas strings.Pipe
e lendo usando umStreamReader
, bem como passando-o como umStream
diretamente eCAST
enviando-o para o servidor.This was tested using SqlClient v6.0.1, and LocalDB 2019 RTM-CU29-GDR. I did not test using TDS v8.0, but some improvements have been made there to streaming large unknown-length parameters.
O tópico Exemplo: Streaming para SQL Server mostra como parâmetros podem ser definidos a partir de um fluxo.
Embora
System.Text.Json
seja possível serializar para um fluxo, não ficou claro para mim se/como é possível fazer com que ele serialize um enumerável e tenha a avaliação do enumerável adiada até ser usada como um valor de parâmetro para o procedimento armazenado equivalente.Eu encontrei uma solução possível para isso nesta resposta do StackOverflow . O abaixo é (agora muito vagamente) baseado nisso e adia a avaliação do enumerável até depois que o
ExecuteScalarAsync
é chamado. No processo do cliente, a memória é muito baixa durante todo o processo.O código de passagem de parâmetros provavelmente ainda tem algumas melhorias de desempenho que podem ser feitas, mas isso não ajudará no desempenho da consulta.
A consulta TVP foi executada em DOP 4 em modo batch na minha máquina e teve uma média de 80 ms por duração mostrada no armazenamento de consultas, enquanto a JSON obteve um plano serial e duração média de > 20 segundos. Na prática, porém, é improvável que alguém envie 5 milhões de elementos JSON para o SQL Server apenas para contá-los e a diferença pode ser bem menos gritante em casos de uso mais realistas.