AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 344924
Accepted
Martin Smith
Martin Smith
Asked: 2025-01-26 03:28:24 +0800 CST2025-01-26 03:28:24 +0800 CST 2025-01-26 03:28:24 +0800 CST

Um array JSON pode ser enviado como um parâmetro de procedimento armazenado em modo de streaming?

  • 772

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 ExecuteScalarAsyncchamada 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);
sql-server
  • 2 2 respostas
  • 219 Views

2 respostas

  • Voted
  1. Best Answer
    Charlieface
    2025-03-30T09:41:44+08:002025-03-30T09:41:44+08:00

    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:

    • Usamos a API de sincronização para o SqlClient, pois a API assíncrona atualmente tem uma séria desvantagem de desempenho.
    • Para garantir que estamos realmente lendo todas as três colunas, adicionei uma subconsulta derivada SELECT DISTINCTsobre todas as colunas.
    [Params(1, 100, 1000, 50000)]
    public uint Length { get; set;}
    
    [Benchmark]
    public int JsonTest()
    {
        const string query = """
                                SELECT COUNT(*)
                                FROM  (
                                    SELECT DISTINCT Prop1, Prop2, Prop3
                                    FROM OPENJSON(@JsonData)
                                    WITH (
                                        Prop1 int,
                                        Prop2 datetime2(7),
                                        Prop3 nvarchar(1000)
                                    ) j
                                ) t;
                             """;
        using var conn = new SqlConnection(connectionString);
        conn.Open();
        using var cmd = new SqlCommand(query, conn);
        cmd.CommandTimeout = 0;
    
        var data = GetEnumerableOfRandomDto(Length);
        using var reader = new StreamReader(GetJsonStream(data));
        cmd.Parameters.Add("@JsonData", SqlDbType.NVarChar, -1).Value = reader;
    
        return (int)cmd.ExecuteScalar();
    }
    
    • O DTO é criado usando valores idênticos para prop2e prop3, para evitar sobrecargas de Randome interpolação de strings.
    static IEnumerable<Dto> GetEnumerableOfRandomDto(uint length)
    {
        var rnd = new Random();
        var s = Guid.NewGuid().ToString();
        var dt = DateTime.Now.AddMinutes(rnd.Next(1, 10000));
    
        for (var i = 0; i < length; i++)
        {
            yield return new Dto(i,
                dt,
                s
            );
        }
    }
    
    • Obtemos o TextReaderuso da nova System.IO.PipelinesAPI da seguinte forma:
      • Crie um Pipe.
      • Em um thread separado, envie os DTOs para o pipe writer o mais rápido que ele puder.
      • Obtenha o leitor de pipe como a Streame passe-o para StreamReader.
      • StreamIsso evita ter que usar toda essa complicação de passagem falsa , já que o Pipefaz tudo isso para nós.
    public static Stream GetJsonStream<T>(T obj, JsonSerializerOptions? options = null)
    {
        var pipe = new Pipe();
        _ = Task.Run(() =>
        {
            try
            {
                var utf8writer = new Utf8JsonWriter(pipe.Writer);
                JsonSerializer.Serialize(utf8writer, obj);
                pipe.Writer.Complete();
            }
            catch(Exception ex)
            {
                pipe.Writer.Complete(ex);
            }
        });
        var stream = pipe.Reader.AsStream();
        return stream;
    }
    
    • Para TvpTest, você não deve recriar SqlDataRecord. Ao contrário de qualquer outro uso de IEnumerableno planeta, é recomendado reutilizar o SqlDataRecord. Observe que a DateTime2escala é explicitamente especificada.
    static IEnumerable<SqlDataRecord> GetRecordsOfRandomDto(uint length)
    {
        var list = GetEnumerableOfRandomDto(length);
        var record = new SqlDataRecord(
            new("prop1", SqlDbType.Int),
            new("prop2", SqlDbType.DateTime2, 0, 7),
            new("prop3", SqlDbType.NVarChar, 1000));
    
        foreach (var dto in list)
        {
            record.SetInt32(0, dto.Prop1);
            record.SetDateTime(1, dto.Prop2);
            record.SetString(2, dto.Prop3);
            yield return record;
        }
    }
    

    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.

    Método Comprimento Significar Erro Desvio Padrão Mediana Gen0 Gen1 Alocado
    Teste Json 1 235,5 μs 9,78 μs 27,41 μs 229,0 μs 2.9297 - 10,07 KB de conteúdo
    Teste de TVP 1 1.014,8 μs 36,34 μs 102,49 μs 991,1 μs 3.9063 - 12,26 KB de conteúdo
    Teste Json 100 929,4 μs 21,54 μs 60,03 μs 921,2 μs 3.9063 - 16,19 KB de conteúdo
    Teste de TVP 100 1.283,3 μs 58,77 μs 168,62 μs 1.230,0 μs 5.8594 - 21,54 KB de conteúdo
    Teste Json 1000 5.925,9 μs 117,72 μs 243,12 μs 5.898,3 μs 15.6250 - 54,16 KB de conteúdo
    Teste de TVP 1000 2.449,4 μs 43,35 μs 96,07 μs 2.424,2 μs 31.2500 - 106,3 KB de conteúdo
    Teste Json 50000 398.435,2 μs 7.684,96 μs 11.021,55 μs 397.393,5 μs 1000.0000 - 6877,39 KB
    Teste de TVP 50000 47.100,7 μs 831,21 μs 736,85 μs 47.121,9 μs 909.0909 545.4545 5045,5 KB de espaço

    Também testei com uma única Prop1 intcoluna em ambos, com a consulta SELECT MAX(Prop1) FROMpara 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.

    Método Comprimento Significar Erro Desvio Padrão Mediana Gen0 Gen1 Alocado
    Teste Json 1 249,2 μs 13,82 μs 38,06 μs 246,8 μs 2.9297 - 10,07 KB de conteúdo
    Teste de TVP 1 1.003,7 μs 39,79 μs 110,26 μs 984,1 μs 0,9766 - 5,33 KB de conteúdo
    Teste Json 100 743,7 μs 19,87 μs 54,05 μs 738,7 μs 4.8828 - 16,21 KB de conteúdo
    Teste de TVP 100 966,3 μs 28,30 μs 76,98 μs 937,2 μs 0,9766 - 5,33 KB de conteúdo
    Teste Json 1000 4.167,8 μs 83,25 μs 197,85 μs 4.179,7 μs 15.6250 - 54,16 KB de conteúdo
    Teste de TVP 1000 1.413,6 μs 21,73 μs 22,32 μs 1.415,0 μs - - 5,33 KB de conteúdo
    Teste Json 50000 282.662,2 μs 5.414,21 μs 13.780,90 μs 279.611,1 μs 1000.0000 500.0000 6874,92 KB de conteúdo
    Teste de TVP 50000 19.903,0 μs 393,06 μs 482,71 μs 19.872,4 μs - - 5,47 KB de conteúdo

    Outras notas:

    • Usar a StreamReadertem a desvantagem de converter de UTF-8 para UTF-16. Eu vi uma pequena melhora em alguns casos usando a varbinarye escrevendo o UTF-8 Streamdiretamente no servidor, então CASTing server-side as varchare então como nvarchar, mas não foi consistente. Infelizmente, o SqlClient não aceita a Streamde bytes UTF-8 para um varcharvalor.
    • Infelizmente, não há como usar System.Text.JsonUTF-16 para escrever, e OPENJSONele só aceita nvarchar, então é quase certo que haja alguma perda de eficiência na conversão.
    • Também tentei usar XML e .nodesvários .valuesmétodos. Todos foram menos eficientes do que os outros dois métodos em todos os casos.
      • XmlReaderprecisa que stringvalores sejam passados, o que significa que muitas strings são geradas para os valores intand convertidos DateTime. Isso ainda pode ser uma opção para um caso de uso que envolva apenas strings.
      • XmlSerializerusará apenas uma XmlReadersaída.
      • XDocumentpode gerar um XmlReader, mas você ainda precisa gerar todas essas strings.
      • Tentei gerar o XML manualmente em UTF-8 e UTF-16 usando o Pipee lendo usando um StreamReader, bem como passando-o como um Streamdiretamente e CASTenviando-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.

    • 5
  2. Martin Smith
    2025-01-26T03:28:24+08:002025-01-26T03:28:24+08:00

    O tópico Exemplo: Streaming para SQL Server mostra como parâmetros podem ser definidos a partir de um fluxo.

    Embora System.Text.Jsonseja 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.

    CREATE OR ALTER PROC dbo.TestJsonPerf
    @JsonData NVARCHAR(MAX)
    AS
    
    SELECT COUNT(*)
    FROM OPENJSON(@JsonData)
    WITH (Prop1 int, Prop2 datetime2, Prop3 varchar(1000))
    

    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 desempenho desse método ainda é pior do que usar TVPs. Com o código TVP na questão na minha máquina local, uma execução típica com 5 milhões de linhas levou de 8 a 9 segundos, a versão de streaming JSON levou cerca de 45 segundos. Dos quais cerca de 25 segundos pareceram ser gastos enviando o valor do parâmetro, então tanto a passagem do parâmetro quanto os tempos de execução da consulta foram muito piores!

    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.

    using System.Data;
    using System.Text;
    using System.Text.Json;
    using Microsoft.Data.SqlClient;
    
    const string connectionString =
        @"...";
    
    await JsonTest();
    
    return;
    
    static async Task JsonTest()
    {
        await using var conn = new SqlConnection(connectionString);
        await conn.OpenAsync();
        await using var cmd = new SqlCommand("dbo.TestJsonPerf", conn);
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.CommandTimeout = 300;
    
    
        var data = GetEnumerableOfRandomDto(5_000_000);
        var stream = new JsonArrayStream<Dto>(data);
    
        cmd.Parameters.Add("@JsonData", SqlDbType.NVarChar, -1).Value =
            new StreamReader(stream, Encoding.UTF8, bufferSize: 1_048_576);
    
        Console.WriteLine($"calling ExecuteScalarAsync at {DateTime.Now:O}");
        var result = await cmd.ExecuteScalarAsync();
    
        Console.WriteLine($"writing result at {DateTime.Now:O}. Result was {result}");
    }
    
    static IEnumerable<Dto> GetEnumerableOfRandomDto(uint length)
    {
        var rnd = new Random();
    
        for (var i = 0; i < length; i++)
        {
            yield return new Dto(i,
                DateTime.UtcNow.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);
    
    public class JsonArrayStream<T> : Stream, IDisposable
    {
        private readonly IEnumerator<T> _input;
        private bool _arrayClosed;
    
        private byte[] _bytesToAppendNextRead = [(byte) '['];
        private bool _firstElement = true;
        private bool _disposed;
    
        public JsonArrayStream(IEnumerable<T> input)
        {
            _input = input.GetEnumerator();
        }
    
        public override bool CanRead => true;
        public override bool CanSeek => false;
        public override bool CanWrite => false;
        public override long Length => 0;
        public override long Position { get; set; }
    
        void IDisposable.Dispose()
        {
            if (_disposed)
                return;
            _disposed = true;
            _input.Dispose();
        }
    
        public override int Read(byte[] buffer, int offset, int count)
        {
            var bytesWrittenThisRead = 0;
    
            if (!TryAppendToBuffer(_bytesToAppendNextRead, buffer, ref offset, count, ref bytesWrittenThisRead))
                return bytesWrittenThisRead;
    
      
            while (_input.MoveNext())
            {
                if (!_firstElement)
                {
                    //regardless of result of this call will proceed to the next step to ensure bytes for _input.Current are saved before MoveNext called
                    TryAppendToBuffer([(byte) ','], buffer, ref offset, count, ref bytesWrittenThisRead);
                }
    
                _firstElement = false;
    
                if (!TryAppendToBuffer(JsonSerializer.SerializeToUtf8Bytes(_input.Current), buffer, ref offset, count, ref bytesWrittenThisRead))
                    return bytesWrittenThisRead;
            }
    
            if (!_arrayClosed)
            {
                TryAppendToBuffer([(byte)']'], buffer, ref offset, count, ref bytesWrittenThisRead);
                _arrayClosed = true;
            }
    
            return bytesWrittenThisRead;
        }
    
        //Appends to stream buffer if possible. If no capacity saves any excess in _bytesToAppendNextRead
        private bool TryAppendToBuffer(byte[] bytesToAppend, byte[] buffer, ref int offset, int count,
            ref int bytesWrittenThisRead)
        {
            var bytesToAppendLength = bytesToAppend.Length;
    
            if (bytesToAppendLength > 0)
            {
                var lengthToWrite = Math.Min(bytesToAppendLength, count - bytesWrittenThisRead);
                Buffer.BlockCopy(bytesToAppend, 0, buffer, offset, lengthToWrite);
                offset += lengthToWrite;
                bytesWrittenThisRead += lengthToWrite;
    
                if (bytesToAppendLength > lengthToWrite)
                {
                    _bytesToAppendNextRead = _bytesToAppendNextRead.Length == 0 ? 
                        bytesToAppend[lengthToWrite..] : 
                        _bytesToAppendNextRead.Concat(bytesToAppend[lengthToWrite..]).ToArray();
    
                    return false;
                }
    
                _bytesToAppendNextRead = [];
            }
    
            return true;
        }
    
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException();
        }
    
        public override void SetLength(long value)
        {
            throw new NotSupportedException();
        }
    
        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException();
        }
    
        public override void Flush()
        {
            throw new NotSupportedException();
        }
    }
    
    • 4

relate perguntas

  • SQL Server - Como as páginas de dados são armazenadas ao usar um índice clusterizado

  • Preciso de índices separados para cada tipo de consulta ou um índice de várias colunas funcionará?

  • Quando devo usar uma restrição exclusiva em vez de um índice exclusivo?

  • Quais são as principais causas de deadlocks e podem ser evitadas?

  • Como determinar se um Índice é necessário ou necessário

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host

    • 12 respostas
  • Marko Smith

    Como fazer a saída do sqlplus aparecer em uma linha?

    • 3 respostas
  • Marko Smith

    Selecione qual tem data máxima ou data mais recente

    • 3 respostas
  • Marko Smith

    Como faço para listar todos os esquemas no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Jin conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host 2014-12-02 02:54:58 +0800 CST
  • Martin Hope
    Stéphane Como faço para listar todos os esquemas no PostgreSQL? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve