Compartilhar via


Banco de dados de exemplo para OLTP na memória

Aplica-se a:SQL ServerBanco de Dados SQL do Azure

Visão geral

Este exemplo mostra o recurso OLTP na memória. Ele mostra tabelas com otimização de memória e procedimentos armazenados compilados nativamente e pode ser usado para demonstrar os benefícios de desempenho do OLTP na memória.

Observação

Para exibir este artigo do SQL Server 2014 (12.x), consulte Extensões para AdventureWorks que demonstram In-Memory OLTP.

O exemplo a seguir migra cinco tabelas do banco de dados do AdventureWorks2025 para a otimização de memória e inclui uma carga de trabalho de demonstração para o processamento de pedidos de vendas. Você pode usar essa carga de trabalho de demonstração para ver o benefício de desempenho do uso do OLTP na memória em seu servidor.

Na descrição do exemplo, discutimos as compensações feitas na migração das tabelas para o OLTP na memória para considerar os recursos que ainda não têm suporte para tabelas com otimização de memória.

A documentação do exemplo foi estruturada como segue:

Pré-requisitos

  • SQL Server 2016 (13.x)

  • Para testes de desempenho, um servidor com as especificações semelhantes ao ambiente de produção. Para este exemplo específico, você deve ter pelo menos 16 GB de memória disponível para o SQL Server. Para obter diretrizes gerais sobre hardware para OLTP na memória, consulte a seguinte postagem no blog: Considerações de hardware para In-Memory OLTP no SQL Server

Instale o exemplo OLTP em memória baseado no AdventureWorks

Siga estas etapas para instalar o exemplo:

  1. Baixe AdventureWorks2016_EXT.bak e SQLServer2016Samples.zip de: https://github.com/microsoft/sql-server-samples/releases/tag/adventureworks para uma pasta local, por exemplo C:\Temp.

  2. Restaure o backup do banco de dados usando o Transact-SQL ou o SQL Server Management Studio:

    1. Identifique a pasta de destino e o nome do arquivo de dados, por exemplo:

      H:\DATA\AdventureWorks2022_Data.mdf
      
    2. Identifique a pasta de destino e o nome do arquivo para o arquivo de log, por exemplo:

      I:\DATA\AdventureWorks2022_log.ldf
      
      1. O arquivo de log deve ser colocado em uma unidade que não seja a do arquivo de dados, preferencialmente uma unidade de baixa latência como um armazenamento de SSD ou de PCIe, para obter o desempenho máximo.

    Exemplo de script T-SQL:

    RESTORE DATABASE [AdventureWorks2022]
      FROM DISK = N'C:\temp\AdventureWorks2022.bak'
        WITH FILE = 1,
      MOVE N'AdventureWorks2022_Data' TO N'h:\DATA\AdventureWorks2022_Data.mdf',
      MOVE N'AdventureWorks2022_Log' TO N'i:\DATA\AdventureWorks2022_log.ldf',
      MOVE N'AdventureWorks2022_mod' TO N'h:\data\AdventureWorks2022_mod'
     GO
    
  3. Para exibir os scripts de exemplo e a carga de trabalho, descompacte o arquivo SQLServer2016Samples.zip em uma pasta local. Consulte o arquivo In-Memory OLTP\readme.txt para obter instruções sobre como executar a carga de trabalho.

Descrição das tabelas e procedimentos de exemplo

O exemplo cria tabelas para produtos e pedidos de vendas, com base em tabelas existentes no AdventureWorks2025. O esquema das novas tabelas é semelhante às tabelas existentes, com algumas diferenças, conforme explicado posteriormente nesta seção.

As novas tabelas com otimização de memória carregam o sufixo _inmem. O exemplo também inclui tabelas correspondentes que carregam o sufixo _ondisk – essas tabelas podem ser usadas para fazer uma comparação um-para-um entre o desempenho de tabelas com otimização de memória e tabelas baseadas em disco em seu sistema.

As tabelas com otimização de memória usadas na carga de trabalho para a comparação de desempenho são totalmente duráveis e registradas. Eles não sacrificam durabilidade ou confiabilidade para obter o ganho de desempenho.

A carga de trabalho de destino para esse exemplo é o processamento de pedidos de vendas, onde também consideramos informações sobre produtos e descontos. Para esse fim, usamos as tabelas SalesOrderHeader, SalesOrderDetail, Product, SpecialOffer e SpecialOfferProduct.

Dois novos procedimentos armazenados, Sales.usp_InsertSalesOrder_inmem e Sales.usp_UpdateSalesOrderShipInfo_inmem, são usados para inserir pedidos de venda e atualizar as informações de envio de um determinado pedido de venda.

O novo esquema Demo contém tabelas e procedimentos armazenados auxiliares para executar uma carga de trabalho de demonstração.

Concretamente, a amostra do Na Memória OLTP adiciona os seguintes objetos ao AdventureWorks2025:

Tabelas adicionadas pelo exemplo

As novas tabelas

Sales.SalesOrderHeader_inmem

  • Informações de cabeçalho sobre os pedidos de vendas. Cada pedido de vendas tem uma linha nesta tabela.

Sales.SalesOrderDetail_inmem

  • Detalhes dos pedidos de vendas. Cada item de linha de um pedido de vendas tem uma linha nesta tabela.

Sales.SpecialOffer_inmem

  • Informações sobre as ofertas especiais, incluindo o percentual de desconto associado a cada oferta especial.

Sales.SpecialOfferProduct_inmem

  • Tabela de referência entre ofertas especiais e produtos. Cada oferta especial pode caracterizar zero ou mais produtos, e cada produto pode ser caracterizado em zero ou mais ofertas especiais.

Production.Product_inmem

  • Informações sobre os produtos, inclusive o preço da lista.

Demo.DemoSalesOrderDetailSeed

  • Usado na carga de trabalho de demonstração para criar exemplos de pedidos de vendas.

Variações das tabelas baseadas em disco:

  • Sales.SalesOrderHeader_ondisk

  • Sales.SalesOrderDetail_ondisk

  • Sales.SpecialOffer_ondisk

  • Sales.SpecialOfferProduct_ondisk

  • Production.Product_ondisk

Diferenças entre as tabelas originais baseadas em disco e as novas tabelas com otimização de memória

Normalmente, as novas tabelas introduzidas por este exemplo usam as mesmas colunas e os mesmos tipos de dados que as tabelas originais. No entanto, há algumas diferenças. Listamos as diferenças nesta seção, juntamente com uma lógica para as alterações.

Sales.SalesOrderHeader_inmem

  • Restrições padrão têm suporte para tabelas com otimização de memória, e migramos a maioria das restrições padrão como tal. No entanto, a tabela Sales.SalesOrderHeader original contém duas restrições padrão que recuperam a data atual, para as colunas OrderDate e ModifiedDate. Em uma carga de trabalho de processamento de pedidos com alta taxa de transferência e muita simultaneidade, qualquer recurso global pode se tornar um ponto de contenção. O tempo do sistema é um recurso global tão importante, e observamos que ele pode ser um ponto de estrangulamento quando estamos executando uma carga de trabalho OLTP In-Memory que insere pedidos de vendas, especialmente se o tempo do sistema precisar ser recuperado para várias colunas no cabeçalho do pedido de vendas e nos detalhes do pedido de vendas. O problema é tratado neste exemplo através da recuperação do tempo do sistema apenas uma vez para cada pedido de vendas que é inserido, e o uso desse valor para as colunas de data e hora em SalesOrderHeader_inmem e em SalesOrderDetail_inmem, no procedimento armazenado Sales.usp_InsertSalesOrder_inmem.

  • UDTs (Tipos de dados definidos pelo usuário) de alias – a tabela original usa dois UDTs de alias dbo.OrderNumber e dbo.AccountNumber, para as colunas PurchaseOrderNumber e AccountNumber, respectivamente. O SQL Server 2016 (13.x) não dá suporte ao Alias UDT para tabelas com otimização de memória, portanto, as novas tabelas usam os tipos de dados do sistema nvarchar(25) e nvarchar(15), respectivamente.

  • Colunas anuláveis em chaves de índice – na tabela original, a coluna SalesPersonID é anulável, enquanto nas novas tabelas a coluna não é anulável e tem uma restrição padrão com valor (-1). Essa circunstância ocorre porque índices em tabelas com otimização de memória não podem ter colunas anuláveis na chave de índice; -1 é um substituto para NULL nesse caso.

  • Colunas computadas – As colunas computadas e SalesOrderNumber são omitidasTotalDue, pois o SQL Server 2016 (13.x) não dá suporte a colunas computadas em tabelas com otimização de memória. A nova exibição Sales.vSalesOrderHeader_extended_inmem reflete as colunas SalesOrderNumber e TotalDue. Por disso, você pode usar essa exibição se essas colunas são necessárias.

    • Aplica-se a: SQL Server 2017 (14.x). A partir do SQL Server 2017 (14.x), há suporte para colunas computadas em índices e tabelas com otimização de memória.
  • Restrições de chave estrangeira são compatíveis com tabelas com otimização de memória no SQL Server 2016 (13 x), mas somente se as tabelas referenciadas também têm otimização de memória. Chaves estrangeiras que referenciam tabelas que também são migradas para a otimização de memória são mantidas nas tabelas migradas, enquanto outras chaves estrangeiras são omitidas. Além disso, SalesOrderHeader_inmem é uma tabela frequente na carga de trabalho de exemplo, e restrições de chaves estrangeiras exigem processamento extra para todas as operações DML, pois exige pesquisas em todas as outras tabelas referenciadas nessas restrições. Portanto, a suposição é que o aplicativo garante a integridade referencial da Sales.SalesOrderHeader_inmem tabela e a integridade referencial não é validada quando as linhas são inseridas.

  • Rowguid - a coluna rowguid é omitida. Embora o uniqueidentifier tenha suporte para tabelas com otimização de memória, a opção ROWGUIDCOL não tem suporte no SQL Server 2016 (13.x). As colunas desse tipo são geralmente usadas para a replicação de mesclagem ou para tabelas com colunas filestream. Este exemplo não inclui isso.

Sales.SalesOrderDetail

  • Restrições padrão - semelhantes a SalesOrderHeader, a restrição padrão que exige a data/hora do sistema não é migrada. Em vez disso, o procedimento armazenado que insere pedidos de vendas cuida da inserção da data/hora do sistema atual na primeira inserção.

  • Colunas computadas – a coluna LineTotal computada não foi migrada, pois não há suporte para colunas computadas com tabelas com otimização de memória no SQL Server 2016 (13.x). Para acessar esta coluna, use a exibição Sales.vSalesOrderDetail_extended_inmem .

  • Rowguid – a coluna rowguid é omitida. Para obter detalhes, confira a descrição da tabela SalesOrderHeader.

Production.Product

  • UDTs de alias – a tabela original usa tipo de dados definidos pelo usuário dbo.Flag, que equivalem ao tipo de dados bit do sistema. A tabela migrada usa o tipo de dados bit.

  • Rowguid – a coluna rowguid é omitida. Para obter detalhes, confira a descrição da tabela SalesOrderHeader.

Sales.SpecialOffer

  • Rowguid – a coluna rowguid é omitida. Para obter detalhes, confira a descrição da tabela SalesOrderHeader.

Sales.SpecialOfferProduct

  • Rowguid – a coluna rowguid é omitida. Para obter detalhes, confira a descrição da tabela SalesOrderHeader.

Considerações para índices em tabelas com otimização de memória

O índice de linha de base para tabelas com otimização de memória é o índice NONCLUSTERED, que dá suporte a pesquisas de ponto (busca de índice em predicado de igualdade), exames de intervalo (busca de índice em predicado de desigualdade), verificações de índice completo e exames ordenados. Além disso, os índices NONCLUSTERED dão suporte à pesquisa nas colunas principais da chave de índice. De fato, os índices NONCLUSTERED com otimização de memória dão suporte a todas as operações com o suporte de índices NONCLUSTERED baseados em disco, exceto apenas para verificações regressivas. Portanto, o uso de índices NONCLUSTERED é uma opção confiável para seus índices.

Os índices de HASH podem ser usados para otimizar mais a carga de trabalho. Eles são otimizados para pesquisas de ponto e inserções de linhas. No entanto, deve-se considerar que eles não dão suporte a verificações de intervalo, verificações ordenadas ou pesquisa em colunas de chave de índice principais. Consequentemente, tome cuidado ao usar esses índices. Além disso, é necessário especificar o bucket_count no momento da criação. Normalmente, ele deve ser definido entre uma e duas vezes o número de valores de chave de índice, mas valores superestimados não costumam ser um problema.

Para obter mais informações:

Os índices nas tabelas migradas foram ajustados para o processamento de pedidos de venda para demonstração. A carga de trabalho depende de inserções e pesquisas de ponto nas tabelas Sales.SalesOrderHeader_inmem e Sales. SalesOrderDetail_inmem também depende de pesquisas de ponto nas colunas de chave primária nas tabelas Production.Product_inmem e Sales.SpecialOffer_inmem.

Sales.SalesOrderHeader_inmem tem três índices, que são todos os índices de HASH por motivos de desempenho, e porque exames ordenados ou de intervalo não são necessários para a carga de trabalho.

  • Índice de HASH em (SalesOrderID): o bucket_count é dimensionado em 10 milhões (arredondado para 16 milhões), pois o número esperado de pedidos de vendas é de 10 milhões

  • Índice de HASH em (SalesPersonID): o bucket_count é de 1 milhão. O conjunto de dados fornecido não tem muitos vendedores. Porém, essa bucket_count grande permite crescimento futuro. Além disso, você não pagará uma penalidade de desempenho por pesquisas de ponto se a bucket_count estiver superdimensionada.

  • Índice de HASH em (CustomerID): o bucket_count é de 1 milhão. O conjunto de dados fornecido não tem muitos clientes, mas isso permite um crescimento futuro.

Sales.SalesOrderDetail_inmem tem três índices, que são todos os índices de HASH por motivos de desempenho, e porque exames ordenados ou de intervalo não são necessários para a carga de trabalho.

  • Índice HASH em (SalesOrderID, SalesOrderDetailID): este é o índice de chave primária e, mesmo que as pesquisas em (SalesOrderID, SalesOrderDetailID) sejam pouco frequentes, usar um índice de hash para a chave acelera as inserções de linha. A bucket_count é dimensionada em 50 milhões (arredondado para 67 milhões): o número esperado de pedidos de vendas é de 10 milhões e esse valor é dimensionado para ter uma média de cinco itens por pedido

  • Índice HASH em (SalesOrderID): pesquisas por ordem de vendas são frequentes: você deseja encontrar todos os itens de linha correspondentes a um único pedido. O bucket_count é dimensionado em 10 milhões (arredondado para 16 milhões), pois o número esperado de pedidos de vendas é de 10 milhões

  • Índice de HASH em (ProductID): o bucket_count é de 1 milhão. O conjunto de dados fornecido não tem muitos produtos, mas isso permite um crescimento futuro.

Production.Product_inmem tem três índices

  • Índice de HASH em 9ProductID): as pesquisas em ProductID estão no caminho crítico para a carga de trabalho de demonstração; então, este é um índice hash

  • Índice NÃO AGRUPADO em (Name): permite varreduras ordenadas de nomes de produtos

  • Índice NONCLUSTERED em (ProductNumber): isso permite verificações ordenadas de números de produto

Sales.SpecialOffer_inmem tem um índice da HASH em (SpecialOfferID): as pesquisas de ponto de ofertas especiais estão na parte essencial da carga de trabalho de demonstração. O bucket_count é dimensionado em 1 milhão para permitir futuro crescimento.

Sales.SpecialOfferProduct_inmem não é referenciado na carga de trabalho de demonstração e, portanto, não há nenhuma necessidade aparente de usar índices de hash nesta tabela para otimizar a carga de trabalho - os índices em (SpecialOfferID, ProductID) e (ProductID) são NONCLUSTERED.

No exemplo anterior, algumas das contagens de buckets são superdimensionadas, mas não as contagens de bucket para os índices SalesOrderHeader_inmem e SalesOrderDetail_inmem: elas são dimensionadas para apenas 10 milhões de pedidos de vendas. Isso foi feito para permitir a instalação do exemplo em sistemas com baixa disponibilidade de memória, embora, nesses casos, a carga de trabalho de demonstração falhe com um erro de memória insuficiente. Se você quiser dimensionar além de 10 milhões de pedidos de vendas, sinta-se à vontade para aumentar o número de buckets de forma correspondente.

Considerações sobre a utilização de memória

A utilização de memória no banco de dados de exemplo, antes e após executar a carga de trabalho de demonstração, é abordada na seção Utilização de memória para as tabelas com otimização de memória.

Procedimentos armazenados adicionados pelo exemplo

Os dois principais procedimentos armazenados para inserir o pedido de vendas e atualizar detalhes do envio são os seguintes:

  • Sales.usp_InsertSalesOrder_inmem

    • Insere um novo pedido de vendas no banco de dados e gera o SalesOrderID para esse pedido. Como parâmetros de entrada, ele recebe detalhes do cabeçalho do pedido de vendas e dos itens de linha do pedido.

    • Parâmetro de saída:

      • @SalesOrderID int – o SalesOrderID pedido de vendas que acabou de ser inserido
    • Parâmetros de entrada (obrigatório):

      • @DueDatedatetime2
      • @CustomerIDint
      • @BillToAddressIDint
      • @ShipToAddressIDint
      • @ShipMethodIDint
      • @SalesOrderDetailsSales.SalesOrderDetailType_inmem - parâmetro com valor de tabela (TVP) que contém os itens de linha do pedido
    • Parâmetros de entrada (opcional):

      • @Statustinyint
      • @OnlineOrderFlagbit
      • @PurchaseOrderNumbernvarchar(25)
      • @AccountNumbernvarchar(15)
      • @SalesPersonIDint
      • @TerritoryIDint
      • @CreditCardIDint
      • @CreditCardApprovalCodevarchar(15)
      • @CurrencyRateIDint
      • @Commentnvarchar(128)
  • Sales.usp_UpdateSalesOrderShipInfo_inmem

    • Atualize as informações de envio de um pedido de vendas específico. Isso também atualiza as informações de envio de todos os itens de linha do pedido de vendas.

    • Este é um procedimento de wrapper para os procedimentos armazenados compilados nativamente Sales.usp_UpdateSalesOrderShipInfo_native, com a lógica de repetição para lidar com conflitos em potencial (inesperados) em transações simultâneas que atualizam o mesmo pedido. Para obter mais informações, confira Lógica de repetição.

  • Sales.usp_UpdateSalesOrderShipInfo_native

    • Este é o procedimento armazenado compilado nativamente que efetivamente processa a atualização das informações de envio. Ele deve ser chamado a partir do procedimento armazenado do wrapper Sales.usp_UpdateSalesOrderShipInfo_inmem. Se o cliente pode tratar as falhas e implementa a lógica de repetição, você pode chamar esse procedimento diretamente, e não usar o procedimento armazenado de wrapper.

O procedimento armazenado a seguir é usado para a carga de trabalho de demonstração.

  • Demo.usp_DemoReset

    • Redefine a demonstração esvaziando e propagando novamente as tabelas SalesOrderHeader e SalesOrderDetail.

Os procedimentos armazenados a seguir são usados para inserir e excluir das tabelas com otimização de memória para garantir o domínio e a integridade referencial.

  • Production.usp_InsertProduct_inmem
  • Production.usp_DeleteProduct_inmem
  • Sales.usp_InsertSpecialOffer_inmem
  • Sales.usp_DeleteSpecialOffer_inmem
  • Sales.usp_InsertSpecialOfferProduct_inmem

Finalmente, o procedimento armazenado a seguir é usado para verificar o domínio e a integridade referencial.

  1. dbo.usp_ValidateIntegrity

    • Parâmetro opcional: @object_id – ID do objeto para validar a integridade para

    • Esse procedimento depende das tabelas dbo.DomainIntegrity, dbo.ReferentialIntegrity e dbo.UniqueIntegrity para as regras de integridade que precisam ser verificadas – o exemplo popula essas tabelas com base na verificação, na chave estrangeira e nas restrições exclusivas que existem para as tabelas originais no banco de dados do AdventureWorks2025.

    • Ele se baseia nos procedimentos auxiliares dbo.usp_GenerateCKCheck, dbo.usp_GenerateFKCheck e dbo.GenerateUQCheck para gerar o T-SQL necessário para realizar as verificações de integridade.

Medidas de desempenho usando a carga de trabalho de demonstração

ostress é uma ferramenta de linha de comando que foi desenvolvida pela equipe de suporte do MICROSOFT CSS SQL Server. Essa ferramenta pode ser usada para executar consultas ou executar procedimentos armazenados em paralelo. Você pode configurar o número de threads para executar uma determinada instrução T-SQL em paralelo e especificar quantas vezes a instrução deve ser executada neste thread; ostress gira os threads e executa a instrução em todos os threads em paralelo. Após a conclusão da execução de todos os threads, o ostress relata o tempo necessário para que todos os threads terminem a execução.

Instalar ostress

O ostress é instalado como parte dos utilitários RML (Linguagem de Marcação de Relatório) ; não há nenhuma instalação autônoma para ostress.

Etapas da instalação:

  1. Baixe e execute o pacote de instalação do x64 para os utilitários do RML na página: Baixar RML para SQL Server

  2. Se houver uma caixa de diálogo informando que determinados arquivos estão em uso, selecione "Continuar"

Executar ostress

Ostress é executada do prompt de linha de comando. É mais conveniente executar a ferramenta a partir do Prompt de Comando RML, que é instalado como parte dos Utilitários RML.

Para abrir o Prompt Cmd RML, siga estas instruções:

No Windows, abra o menu Iniciar selecionando a tecla Windows e digite rml. Selecione RML Cmd Prompt, que está na lista de resultados da pesquisa.

Verifique se o prompt de comando está localizado na pasta de instalação Utilitários de RML.

As opções de linha de comando para ostress podem ser vistas ao executá-lo simplesmente ostress.exe sem nenhuma opção de linha de comando. As principais opções a serem consideradas para executar o ostress com este exemplo são as seguintes:

Opção Description
-S Nome da instância do SQL Server à qual se conectar.
-E Usar a autenticação do Windows para se conectar (padrão); se você usar a autenticação do SQL Server, use as opções -U e especifique -P o nome de usuário e a senha, respectivamente.
-d Nome do banco de dados, para este exemplo AdventureWorks2025.
-Q A instrução T-SQL a ser executada.
-n Número de conexões processando cada arquivo/consulta de entrada.
-r O número de iterações para cada conexão para executar cada arquivo/consulta de entrada.

Carga de trabalho de demonstração

O procedimento armazenado principal usado na carga de trabalho de demonstração é Sales.usp_InsertSalesOrder_inmem/ondisk. No exemplo a seguir, o script constrói um parâmetro de valor de tabela (TVP) com dados de exemplo e executa o procedimento para inserir um pedido de venda com cinco itens de linha.

A ferramenta ostress é usada para executar os procedimentos armazenados em paralelo, para simular a inserção simultânea de pedidos de venda por clientes.

Redefina a demonstração após cada execução de estresse executando o Demo.usp_DemoReset. Este procedimento exclui as linhas nas tabelas com otimização de memória, trunca as tabelas baseadas em disco e executa um ponto de verificação de banco de dados.

O seguinte script é executado simultaneamente para simular uma carga de trabalho de processamento de pedido de vendas:

DECLARE @i AS INT = 0, @od AS Sales.SalesOrderDetailType_inmem, @SalesOrderID AS INT, @DueDate AS DATETIME2 = sysdatetime(), @CustomerID AS INT = RAND() * 8000, @BillToAddressID AS INT = RAND() * 10000, @ShipToAddressID AS INT = RAND() * 10000, @ShipMethodID AS INT = (RAND() * 5) + 1;
INSERT INTO @od
SELECT OrderQty,
       ProductID,
       SpecialOfferID
FROM Demo.DemoSalesOrderDetailSeed
WHERE OrderID = CAST ((RAND() * 106) + 1 AS INT);
WHILE (@i < 20)
    BEGIN
        EXECUTE Sales.usp_InsertSalesOrder_inmem
            @SalesOrderID OUTPUT,
            @DueDate,
            @CustomerID,
            @BillToAddressID,
            @ShipToAddressID,
            @ShipMethodID,
            @od;
        SET @i + = 1;
    END

Com esse script, cada pedido de exemplo criado é inserido 20 vezes, com 20 procedimentos armazenados executados em um loop WHILE. O loop é usado para considerar o fato de que o banco de dados é usado para criar o pedido de exemplo. Em ambientes de produção típicos, o aplicativo de camada intermediária constrói a ordem de vendas a ser inserida.

O script anterior insere pedidos de vendas em tabelas com otimização de memória. O script para inserir pedidos de vendas em tabelas baseadas em disco é derivado substituindo as duas ocorrências de _inmem por _ondisk.

Usamos a ferramenta ostress para executar os scripts usando várias conexões simultâneas. Usamos o parâmetro -n para controlar o número de conexões e o parâmetro r para controlar quantas vezes o script é executado em cada conexão.

Executar a carga de trabalho

Para testar na escala, inserimos 10 milhões de pedidos de vendas, usando 100 conexões. Esse teste é executado de forma razoável em um servidor modesto (por exemplo, 8 núcleos físicos, 16 núcleos lógicos), e o armazenamento básico de SSD para o log. Se o teste não tiver um bom desempenho em seu hardware, examine a seção Solucionar problemas de testes de execução lenta. Se você quiser reduzir o nível de estresse para este teste, diminua o número de conexões alterando o parâmetro -n. Por exemplo, para reduzir a contagem de conexões para 40, altere o parâmetro -n100 para -n40.

Como medida de desempenho para a carga de trabalho, usamos o tempo decorrido conforme relatado por ostress.exe após a execução da carga de trabalho.

As instruções e medidas a seguir usam uma carga de trabalho que insere 10 milhões de pedidos de vendas. Para obter instruções para executar uma carga de trabalho reduzida inserindo 1 milhão de pedidos de vendas, consulte as instruções presentes em In-Memory OLTP\readme.txt, como parte do arquivo SQLServer2016Samples.zip.

Tabelas com otimização de memória

Começamos executando a carga de trabalho em tabelas com otimização de memória. O comando a seguir abre 100 threads, cada um executado para 5.000 iterações. Cada iteração insere 20 pedidos de vendas em transações separadas. Há 20 inserções por iteração para compensar o fato de que o banco de dados é usado para gerar os dados a serem inseridos. Isso gera um total de 20 * 5.000 * 100 = 10.000.000 inserções de ordens de venda.

Abra o Prompt do Cmd RML e execute o seguinte comando:

Selecione o botão Copiar para copiar o comando, e cole-o no prompt de comando Utilitários de RML.

ostress.exe -n100 -r5000 -S. -E -dAdventureWorks2022 -q -Q"DECLARE @i AS INT = 0, @od AS Sales.SalesOrderDetailType_inmem, @SalesOrderID AS INT, @DueDate AS DATETIME2 = SYSDATETIME(), @CustomerID AS INT = RAND() * 8000, @BillToAddressID AS INT = RAND() * 10000, @ShipToAddressID AS INT = RAND() * 10000, @ShipMethodID AS INT = (RAND() * 5) + 1; INSERT INTO @od SELECT OrderQty, ProductID, SpecialOfferID FROM Demo.DemoSalesOrderDetailSeed WHERE OrderID = CAST ((RAND() * 106) + 1 AS INT); WHILE (@i < 20) BEGIN EXECUTE Sales.usp_InsertSalesOrder_inmem @SalesOrderID OUTPUT, @DueDate, @CustomerID, @BillToAddressID, @ShipToAddressID, @ShipMethodID, @od; SET @i + = 1; END"

Em um servidor de teste com um número total de 8 núcleos físicos (16 lógicos), isso levou 2 minutos e 5 segundos. Em um segundo servidor de teste com 24 núcleos físicos (48 lógicos), isso levou 1 minuto e 0 segundos.

Observe a utilização da CPU enquanto a carga de trabalho está em execução. Use, por exemplo, o gerenciador de tarefas. Você vê que a utilização da CPU está perto de 100%. Se esse não for o caso, você tem um gargalo de E/S de log, consulte também solução de problemas de testes de execução lenta.

Tabelas baseadas em disco

O comando a seguir executa a carga de trabalho em tabelas baseadas em disco. Essa carga de trabalho pode demorar um pouco para ser executada, o que ocorre em grande parte devido à contenção de trava no sistema. As tabelas com otimização de memória são livres de travas e, portanto, não sofrem com esse problema.

Abra o Prompt Cmd RML e execute o seguinte comando:

Selecione o botão Copiar para copiar o comando, e cole-o no prompt de comando Utilitários de RML.

ostress.exe -n100 -r5000 -S. -E -dAdventureWorks2022 -q -Q"DECLARE @i AS INT = 0, @od AS Sales.SalesOrderDetailType_ondisk, @SalesOrderID AS INT, @DueDate AS DATETIME2 = sysdatetime(), @CustomerID AS INT = RAND() * 8000, @BillToAddressID AS INT = RAND() * 10000, @ShipToAddressID AS INT = RAND() * 10000, @ShipMethodID AS INT = (RAND() * 5) + 1; INSERT INTO @od SELECT OrderQty, ProductID, SpecialOfferID FROM Demo.DemoSalesOrderDetailSeed WHERE OrderID = CAST ((RAND() * 106) + 1 AS INT); WHILE (@i < 20) BEGIN EXECUTE Sales.usp_InsertSalesOrder_ondisk @SalesOrderID OUTPUT, @DueDate, @CustomerID, @BillToAddressID, @ShipToAddressID, @ShipMethodID, @od; SET @i + = 1; END"

Em um servidor de teste com um número total de 8 núcleos físicos (16 lógicos), isso levou 41 minutos e 25 segundos. Em um segundo servidor de teste com 24 núcleos físicos (48 lógicos), isso levou 52 minutos e 16 segundos.

O principal fator na diferença de desempenho entre tabelas com otimização de memória e tabelas baseadas em disco neste teste é que, ao usar tabelas baseadas em disco, o SQL Server não pode utilizar totalmente a CPU. A razão é a contenção de trava: as transações simultâneas estão tentando gravar na mesma página de dados; as travas são usadas para garantir que somente uma transação de cada vez possa gravar em uma página. O mecanismo OLTP In-Memory está livre de travas e as linhas de dados não são organizadas em páginas. Assim, as transações simultâneas não bloqueiam as inserções umas das outras, permitindo que o SQL Server utilize totalmente a CPU.

Você pode observar a utilização da CPU enquanto a carga de trabalho está em execução. Use, por exemplo, o gerenciador de tarefas. Você vê com tabelas baseadas em disco que a utilização da CPU está longe de 100%. Em uma configuração de teste com 16 processadores lógicos, a utilização fica em torno de 24%.

Opcionalmente, você pode exibir o número de esperas de trava por segundo usando o Monitor de Desempenho, com o contador de desempenho \SQL Server:Latches\Latch Waits/sec.

Redefinir a demonstração

Para redefinir a demonstração, abra o Prompt Cmd RML e execute o seguinte comando:

ostress.exe -S. -E -dAdventureWorks2022 -Q"EXEC Demo.usp_DemoReset"

Dependendo do hardware, isso pode levar alguns minutos para ser executado.

Recomendamos uma redefinição após cada demonstração executada. Como essa carga de trabalho é somente de inserção, cada execução consome mais memória e, portanto, uma redefinição é necessária para evitar a perda de memória. A quantidade de memória consumida após uma execução é discutida na seção Utilização de memória após executar a carga de trabalho.

Solucionar problemas de testes de execução lenta

Os resultados do teste normalmente variam com o hardware e também com o nível de simultaneidade usado na execução do teste. Algumas coisas a serem buscadas se os resultados não forem os esperados:

  • Número de transações simultâneas: ao executar a carga de trabalho em um único thread, o ganho de desempenho com In-Memory OLTP provavelmente é menor que 2X. A contenção de trava só será um problema significativo se houver um alto nível de simultaneidade.

  • Número baixo de núcleos disponíveis para o SQL Server: isso significa que há um baixo nível de simultaneidade no sistema, pois só pode haver tantas transações em execução simultâneas quanto houver núcleos disponíveis para o SQL.

    • Sintoma: se a utilização da CPU for alta ao executar a carga de trabalho em tabelas baseadas em disco, isso significa que não há muita contenção, apontando para uma falta de simultaneidade.
  • Velocidade da unidade de log: se a unidade de log não puder acompanhar o throughput de transações no sistema, a carga de trabalho ficará limitada pela E/S do log. Embora o log seja mais eficiente com OLTP in-memory, se a E/S de log é um gargalo, o ganho de desempenho potencial é limitado.

    • Sintoma: se a utilização da CPU não estiver perto de 100% ou estiver muito irregular ao executar a carga de trabalho em tabelas otimizadas para memória, é possível que haja um gargalo de E/S de log. Isso pode ser confirmado abrindo o Monitor de Recursos e examinando o comprimento da fila para a unidade de log.

Utilização de espaço em disco e memória no exemplo

No exemplo a seguir, descrevemos o que esperar em termos de utilização de espaço em disco e memória para o banco de dados de exemplo. Também mostramos os resultados de um servidor de teste com 16 núcleos lógicos.

Utilização de memória para as tabelas com otimização de memória

Utilização geral do banco de dados

A consulta a seguir pode ser usada para obter a utilização total de memória para OLTP in-memory no sistema.

SELECT type,
       name,
       pages_kb / 1024 AS pages_MB
FROM sys.dm_os_memory_clerks
WHERE type LIKE '%xtp%';

Instantâneo logo após a criação do banco de dados:

tipo name pages_MB
MEMORYCLERK_XTP Padrão 94
MEMORYCLERK_XTP DB_ID_5 877
MEMORYCLERK_XTP Padrão 0
MEMORYCLERK_XTP Padrão 0

Os administradores de memória padrão contêm estruturas de memória do sistema e são relativamente pequenos. O gerenciador de memória do banco de dados do usuário, nesse caso, o banco de dados com ID 5 (a ID database_id pode ser diferente na sua instância), tem cerca de 900 MB.

Utilização da memória por tabela

A seguinte consulta pode ser usada para fazer uma busca detalhada na utilização da memória das tabelas individuais e de seus índices:

SELECT object_name(t.object_id) AS [Table name],
       memory_allocated_for_table_kb,
       memory_allocated_for_indexes_kb
FROM sys.dm_db_xtp_table_memory_stats AS dms
     INNER JOIN sys.tables AS t
         ON dms.object_id = t.object_id
WHERE t.type = 'U';

A tabela a seguir mostra os resultados dessa consulta para uma instalação atualizada do exemplo:

Nome da tabela memory_allocated_for_table_kb memory_allocated_for_indexes_kb
SpecialOfferProduct_inmem 64 3840
DemoSalesOrderHeaderSeed 1984 5,504
SalesOrderDetail_inmem 15316 663552
DemoSalesOrderDetailSeed 64 10,432
SpecialOffer_inmem 3 8192
SalesOrderHeader_inmem 7168 147456
Product_inmem 124 12352

Como você pode ver, as tabelas são bastante pequenas: SalesOrderHeader_inmem tem cerca de 7 MB e SalesOrderDetail_inmem tem cerca de 15 MB de tamanho.

O que chama a atenção aqui é o tamanho da memória alocada para índices, comparado ao tamanho dos dados da tabela. Isso ocorre porque os índices de hash na amostra são pré-dimensionados para um tamanho de dados maior. Os índices de hash têm um tamanho fixo e, portanto, seu tamanho não cresce com o tamanho dos dados na tabela.

Utilização de memória após executar a carga de trabalho

Depois de inserir 10 milhões de pedidos de vendas, a utilização de memória total será semelhante à seguinte consulta:

SELECT type,
       name,
       pages_kb / 1024 AS pages_MB
FROM sys.dm_os_memory_clerks
WHERE type LIKE '%xtp%';

Veja aqui o conjunto de resultados.

type name pages_MB
MEMORYCLERK_XTP Padrão 146
MEMORYCLERK_XTP DB_ID_5 7374
MEMORYCLERK_XTP Padrão 0
MEMORYCLERK_XTP Padrão 0

Como você pode notar, o SQL Server está usando um pouco menos de 8 GB para as tabelas e os índices com otimização de memória no banco de dados de exemplo.

Verificando o uso detalhado de memória pela tabela após a execução de um exemplo:

SELECT object_name(t.object_id) AS [Table name],
       memory_allocated_for_table_kb,
       memory_allocated_for_indexes_kb
FROM sys.dm_db_xtp_table_memory_stats AS dms
     INNER JOIN sys.tables AS t
         ON dms.object_id = t.object_id
WHERE t.type = 'U';

Veja aqui o conjunto de resultados.

Table name memory_allocated_for_table_kb memory_allocated_for_indexes_kb
SalesOrderDetail_inmem 5113761 663552
DemoSalesOrderDetailSeed 64 10368
SpecialOffer_inmem 2 8192
SalesOrderHeader_inmem 1575679 147456
Product_inmem 111 12032
SpecialOfferProduct_inmem 64 3712
DemoSalesOrderHeaderSeed 1984 5,504

Podemos ver um total de aproximadamente 6,5 GB de dados. O tamanho dos índices na tabela SalesOrderHeader_inmem e SalesOrderDetail_inmem é o mesmo que o tamanho dos índices antes de inserir os pedidos de vendas. O tamanho do índice não foi alterado porque ambas as tabelas estão usando índices de hash e os índices de hash são estáticos.

Após a redefinição de demonstração

O procedimento armazenado Demo.usp_DemoReset pode ser usado para redefinir a demonstração. Ele exclui os dados nas tabelas SalesOrderHeader_inmem e SalesOrderDetail_inmem e regarrega os dados das tabelas originais SalesOrderHeader e SalesOrderDetail.

Agora, mesmo que as linhas nas tabelas tenham sido excluídas, isso não significa que a memória seja recuperada imediatamente. O SQL Server recupera a memória das linhas excluídas em tabelas com otimização de memória em segundo plano, conforme necessário. Você verá que imediatamente após o reset de demonstração, sem carga de trabalho transacional no sistema, a memória das linhas excluídas ainda não foi recuperada:

SELECT type,
       name,
       pages_kb / 1024 AS pages_MB
FROM sys.dm_os_memory_clerks
WHERE type LIKE '%xtp%';

Veja aqui o conjunto de resultados.

type name pages_MB
MEMORYCLERK_XTP Padrão 2261
MEMORYCLERK_XTP DB_ID_5 7396
MEMORYCLERK_XTP Padrão 0
MEMORYCLERK_XTP Padrão 0

Isso é esperado: a memória é recuperada quando a carga de trabalho transacional está em execução.

Se você iniciar uma segunda execução da carga de trabalho de demonstração, verá a utilização da memória diminuir inicialmente, pois as linhas excluídas anteriormente serão limpas. Em algum momento, o tamanho da memória aumenta novamente até que a carga de trabalho seja concluída. Depois de inserir 10 milhões de linhas após a redefinição da demonstração, a utilização da memória é muito semelhante à utilização após a primeira execução. Por exemplo:

SELECT type,
       name,
       pages_kb / 1024 AS pages_MB
FROM sys.dm_os_memory_clerks
WHERE type LIKE '%xtp%';

Veja aqui o conjunto de resultados.

type name pages_MB
MEMORYCLERK_XTP Padrão 1,863
MEMORYCLERK_XTP DB_ID_5 7390
MEMORYCLERK_XTP Padrão 0
MEMORYCLERK_XTP Padrão 0

Utilização de disco para as tabelas com otimização de memória

O tamanho geral em disco para os arquivos de ponto de verificação de um banco de dados em determinado momento pode ser localizado usando a consulta:

SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups AS f
     INNER JOIN sys.database_files AS df
         ON f.data_space_id = df.data_space_id
WHERE f.type = N'FX';

Estado inicial

Quando o grupo de arquivos de exemplo e as tabelas com otimização de memória de exemplo são criados inicialmente, vários arquivos de ponto de verificação são pré-criados e o sistema começa a preencher os arquivos - o número de arquivos de ponto de verificação pré-criados depende do número de processadores lógicos no sistema. Como o exemplo é inicialmente muito pequeno, os arquivos pré-criados ficam vazios principalmente após a criação inicial.

O código a seguir mostra o tamanho inicial em disco para o exemplo em um computador com 16 processadores lógicos:

SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups AS f
     INNER JOIN sys.database_files AS df
         ON f.data_space_id = df.data_space_id
WHERE f.type = N'FX';

Veja aqui o conjunto de resultados.

Tamanho em disco em MB
2312

Como você pode ver, há uma grande discrepância entre o tamanho em disco dos arquivos de ponto de verificação, que é de 2,3 GB, e o tamanho real dos dados, que é mais próximo de 30 MB.

Levando em conta a origem da utilização do espaço em disco, você pode usar a seguinte consulta. O tamanho do disco retornado por esta consulta é aproximado para os arquivos com estado 5 (NECESSÁRIO PARA BACKUP/AD), 6 (EM TRANSIÇÃO PARA MARCA DE EXCLUSÃO) ou 7 (MARCA DE EXCLUSÃO).

SELECT state_desc,
       file_type_desc,
       COUNT(*) AS [count],
       SUM(CASE WHEN state = 5 AND file_type = 0 THEN 128 * 1024 * 1024
                WHEN state = 5 AND file_type = 1 THEN 8 * 1024 * 1024
                WHEN state IN (6, 7) THEN 68 * 1024 * 1024
           ELSE file_size_in_bytes END) / 1024 / 1024 AS [on-disk size MB]
FROM sys.dm_db_xtp_checkpoint_files
GROUP BY state, state_desc, file_type, file_type_desc
ORDER BY state, file_type;

Para o estado inicial do exemplo, o resultado se parece com a tabela a seguir para um servidor com 16 processadores lógicos:

state_desc file_type_desc count tamanho em disco em MB
PRECREATED DADOS 16 2048
PRECREATED DELTA 16 128
EM CONSTRUÇÃO DADOS 1 128
EM CONSTRUÇÃO DELTA 1 8

Como você pode ver, a maior parte do espaço é usado por dados e arquivos delta criados anteriormente. O SQL Server pré-criou um par de arquivos (dados, delta) por processador lógico. Além disso, os arquivos de dados são pré-dimensionados em 128 MB e arquivos delta a 8 MB, a fim de tornar a inserção de dados nesses arquivos mais eficiente.

Os dados reais nas tabelas com otimização de memória estão no único arquivo de dados.

Após executar a carga de trabalho

Após a execução de um único teste que insere 10 milhões de pedidos de vendas, o tamanho geral em disco tem a seguinte aparência (para um servidor de teste com 16 núcleos):

SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups AS f
     INNER JOIN sys.database_files AS df
         ON f.data_space_id = df.data_space_id
WHERE f.type = N'FX';

Veja aqui o conjunto de resultados.

Tamanho em disco em MB
8828

O tamanho em disco é em torno de 9 GB, um valor próximo ao tamanho dos dados na memória.

Verificando melhor os tamanhos dos arquivos de ponto de verificação em vários estados:

SELECT state_desc,
       file_type_desc,
       COUNT(*) AS [count],
       SUM(CASE WHEN state = 5 AND file_type = 0 THEN 128 * 1024 * 1024
                WHEN state = 5 AND file_type = 1 THEN 8 * 1024 * 1024
                WHEN state IN (6, 7) THEN 68 * 1024 * 1024
            ELSE file_size_in_bytes END) / 1024 / 1024 AS [on-disk size MB]
FROM sys.dm_db_xtp_checkpoint_files
GROUP BY state, state_desc, file_type, file_type_desc
ORDER BY state, file_type;

Veja aqui o conjunto de resultados.

state_desc file_type_desc count on-disk size MB
PRECREATED DADOS 16 2048
PRECREATED DELTA 16 128
EM CONSTRUÇÃO DADOS 1 128
EM CONSTRUÇÃO DELTA 1 8

Ainda temos 16 pares de arquivos pré-criados, prontos para uso conforme os pontos de verificação são encerrados.

Há um par em construção, que é usado até que o ponto de verificação atual seja fechado. Junto com os arquivos de ponto de verificação ativos, isso representa em torno de 6,5 GB de utilização de disco para 6,5 GB de dados na memória. Lembre-se de que os índices não são persistidos no disco e, portanto, o tamanho geral no disco é menor do que o tamanho na memória nesse caso.

Após a redefinição de demonstração

Após a redefinição da demonstração, o espaço em disco não será recuperado imediatamente se não houver nenhuma carga de trabalho transacional no sistema e não houver pontos de verificação de banco de dados. Para que os arquivos de ponto de verificação passem por seus vários estágios e sejam eventualmente descartados, vários pontos de verificação e eventos de truncamento de log precisam ocorrer, a fim de iniciar a mesclagem de arquivos de ponto de verificação, bem como iniciar a coleta de lixo. Isso ocorrerá automaticamente se você tiver uma carga de trabalho transacional no sistema (e fizer backups de log regulares, caso esteja usando o modelo de recuperação FULL), mas não quando o sistema estiver ocioso, como em um cenário de demonstração.

No exemplo, após a redefinição da demonstração, você poderá ver algo como:

SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups AS f
     INNER JOIN sys.database_files AS df
         ON f.data_space_id = df.data_space_id
WHERE f.type = N'FX';

Veja aqui o conjunto de resultados.

Tamanho em disco em MB
11839

Com quase 12 GB, isso é significativamente mais do que os 9 GB que tínhamos antes da redefinição de demonstração. Isso ocorre porque algumas mesclagens de arquivos de ponto de verificação foram iniciadas, mas alguns dos destinos de mesclagem ainda não foram instalados e alguns dos arquivos de origem de mesclagem ainda não foram limpos, como pode ser visto no exemplo a seguir:

SELECT state_desc,
       file_type_desc,
       COUNT(*) AS [count],
       SUM(CASE WHEN state = 5 AND file_type = 0 THEN 128 * 1024 * 1024
                WHEN state = 5 AND file_type = 1 THEN 8 * 1024 * 1024
                WHEN state IN (6, 7) THEN 68 * 1024 * 1024
           ELSE file_size_in_bytes END) / 1024 / 1024 AS [on-disk size MB]
FROM sys.dm_db_xtp_checkpoint_files
GROUP BY state, state_desc, file_type, file_type_desc
ORDER BY state, file_type;

Veja aqui o conjunto de resultados.

state_desc file_type_desc count on-disk size MB
PRECREATED DADOS 16 2048
PRECREATED DELTA 16 128
ACTIVE DADOS 38 5152
ACTIVE DELTA 38 1331
DESTINO DE MESCLAGEM DADOS 7 896
DESTINO DE MESCLAGEM DELTA 7 56
ORIGEM MESCLADA DADOS 13 1,772
ORIGEM MESCLADA DELTA 13 4:55

Os destinos de mesclagem serão instalados e a origem mesclada será limpa à medida que a atividade transacional ocorrer no sistema.

Após uma segunda execução da carga de trabalho de demonstração, inserindo 10 milhões de pedidos de vendas após a redefinição da demonstração, você verá que os arquivos construídos durante a primeira execução da carga de trabalho foram limpos. Se você executar a consulta anterior várias vezes enquanto a carga de trabalho estiver em execução, você poderá ver os arquivos de ponto de verificação percorrerem os vários estágios.

Após a segunda execução da carga de trabalho que insere 10 milhões de pedidos de vendas, você verá que a utilização do disco é muito semelhante, embora não necessariamente igual à que ocorreu após a primeira execução, pois o sistema é dinâmico por natureza. Por exemplo:

SELECT state_desc,
       file_type_desc,
       COUNT(*) AS [count],
       SUM(CASE WHEN state = 5 AND file_type = 0 THEN 128 * 1024 * 1024
                WHEN state = 5 AND file_type = 1 THEN 8 * 1024 * 1024
                WHEN state IN (6, 7) THEN 68 * 1024 * 1024
           ELSE file_size_in_bytes END) / 1024 / 1024 AS [on-disk size MB]
FROM sys.dm_db_xtp_checkpoint_files
GROUP BY state, state_desc, file_type, file_type_desc
ORDER BY state, file_type;

Veja aqui o conjunto de resultados.

state_desc file_type_desc count on-disk size MB
PRECREATED DADOS 16 2048
PRECREATED DELTA 16 128
EM CONSTRUÇÃO DADOS 2 268
EM CONSTRUÇÃO DELTA 2 16
ACTIVE DADOS 41 5608
ACTIVE DELTA 41 328

Nesse caso, há dois pares de arquivos de ponto de verificação no estado UNDER CONSTRUCTION, o que significa que vários pares de arquivos foram movidos para o estado UNDER CONSTRUCTION, provavelmente devido ao alto nível de concorrência na carga de trabalho. Várias threads simultâneas requisitaram um novo par de arquivos ao mesmo tempo e, portanto, moveram um par de PRECREATED para UNDER CONSTRUCTION.