Ícone do site Blog SQLCrespi

Série: Specialized Tables no SQL Server 2025

Sobre a série

São 5 posts independentes, um por tipo de specialized table do SQL Server 2025, pensados como material de estudo para a DP-800. Cada post tem introdução, conceito, laboratório reproduzível (mão na massa), pegadinhas, notas para a prova e conclusão.

ParteTemaVersão MínimaIdeia Central
1In-Memory OLTPSQL Server 2014Tabela que vive na memória, lock-free e latch-free
2Temporal TableSQL Server 2016Histórico automático de cada linha
3External TablesSQL Server 2016 (Polybase)Consultar dados que estão fora do banco
4Ledger TablesSQL Server 2022prova criptográfica de que ninguém adulterou os dados
5Graph TablesSQL Server 2017Nós e arestas para modelar relacionamentos

Pré-requisitos para os labs: uma instância SQL Server 2025 (Developer Edition serve) e o SSMS 21 ou o VS Code com a extensão mssql. Todos os scripts criam e usam o banco DP800_Lab, então rode o bloco abaixo uma vez antes de começar.

Pré-requisitos para os labs: uma instância SQL Server 2025 (Developer Edition serve) e o SSMS 21 ou o VS Code com a extensão mssql. Todos os scripts criam e usam o banco DP800_Lab, então rode o bloco abaixo uma vez antes de começar.

-- Cria o banco de laboratório usado em todos os posts da série
USE master;
GO
IF DB_ID('DP800_Lab') IS NOT NULL
BEGIN
    ALTER DATABASE DP800_Lab SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE DP800_Lab;
END
GO
CREATE DATABASE DP800_Lab;
GO
USE DP800_Lab;
GO

Parte 1 — Specialized Tables no SQL Server 2025: In-Memory OLTP

Introdução

Abrindo a série sobre specialized tables para a DP-800, começamos pela mais “veloz” de todas: as memory-optimized tables do In-Memory OLTP (o antigo codinome Hekaton). A promessa é simples: tirar a tabela do modelo baseado em páginas de 8 KB e colocá-la inteira na memória, sem locks e sem latches. Em cenários de alta concorrência, como fila de pedidos, session state ou ingestão de IoT, o ganho pode ser de várias vezes.

Como funciona

Uma memory-optimized table usa row versioning otimista (MVCC): cada UPDATE gera uma nova versão da linha, e conflitos são detectados no commit, não com bloqueio. Os índices também vivem só em memória e são reconstruídos quando a instância sobe.

Mão na massa

USE DP800_Lab;
GO

-- 1. Toda tabela memory-optimized exige um filegroup MEMORY_OPTIMIZED_DATA
ALTER DATABASE DP800_Lab
    ADD FILEGROUP FG_InMemory CONTAINS MEMORY_OPTIMIZED_DATA;
GO

-- Ajuste o caminho para uma pasta existente na sua máquina
ALTER DATABASE DP800_Lab
    ADD FILE (NAME = 'DP800_InMemory', FILENAME = 'C:\SQLData\DP800_InMemory')
    TO FILEGROUP FG_InMemory;
GO

-- 2. Tabela durável (SCHEMA_AND_DATA) com HASH index na PK
CREATE TABLE dbo.Pedido
(
    PedidoId     INT IDENTITY(1,1) NOT NULL
        PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1048576),
    ClienteId    INT          NOT NULL,
    Valor        DECIMAL(10,2) NOT NULL,
    DataPedido   DATETIME2    NOT NULL DEFAULT SYSUTCDATETIME(),

    -- Índice Bw-tree para consultas por intervalo de data
    INDEX IX_Pedido_Data NONCLUSTERED (DataPedido)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
GO

-- 3. Tabela não durável (SCHEMA_ONLY): os dados somem no restart
CREATE TABLE dbo.SessaoCache
(
    SessaoId   UNIQUEIDENTIFIER NOT NULL
        PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 131072),
    Payload    NVARCHAR(4000)   NULL,
    ExpiraEm   DATETIME2        NOT NULL
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY);
GO

-- 4. Natively compiled stored procedure para inserir pedidos
CREATE OR ALTER PROCEDURE dbo.usp_InserirPedido
    @ClienteId INT,
    @Valor     DECIMAL(10,2)
WITH NATIVE_COMPILATION, SCHEMABINDING
AS
BEGIN ATOMIC WITH
(
    TRANSACTION ISOLATION LEVEL = SNAPSHOT,
    LANGUAGE = N'us_english'
)
    INSERT INTO dbo.Pedido (ClienteId, Valor)
    VALUES (@ClienteId, @Valor);
END;
GO

-- 5. Carga de teste: 100 mil pedidos
SET NOCOUNT ON;
DECLARE @i INT = 1;
WHILE @i <= 100000
BEGIN
    -- EXEC não aceita expressões nos parâmetros, por isso as variáveis
    DECLARE @c INT = @i % 1000,
            @v DECIMAL(10,2) = (@i % 500) + 0.99;
    EXEC dbo.usp_InserirPedido @ClienteId = @c, @Valor = @v;
    SET @i += 1;
END;
GO

-- 6. Conferindo a saúde do HASH index (cadeias longas = BUCKET_COUNT baixo)
SELECT  OBJECT_NAME(hs.object_id)  AS Tabela,
        i.name                     AS Indice,
        hs.total_bucket_count,
        hs.empty_bucket_count,
        hs.avg_chain_length,
        hs.max_chain_length
FROM sys.dm_db_xtp_hash_index_stats AS hs
JOIN sys.indexes AS i
    ON i.object_id = hs.object_id AND i.index_id = hs.index_id;

-- 7. Quanto de memória cada tabela está usando
SELECT  OBJECT_NAME(object_id) AS Tabela,
        memory_allocated_for_table_kb,
        memory_used_by_table_kb
FROM sys.dm_db_xtp_table_memory_stats
WHERE object_id > 0;

WHERE object_id > 0;

Bônus: dá para usar o mesmo motor em memory-optimized table types, substituindo #temp e variáveis de tabela em procedures muito chamadas.

-- Table type em memória: sem tempdb, sem contenção de alocação
CREATE TYPE dbo.tt_ListaIds AS TABLE
(
    Id INT NOT NULL PRIMARY KEY NONCLUSTERED
)
WITH (MEMORY_OPTIMIZED = ON);
GO

DECLARE @ids dbo.tt_ListaIds;
INSERT INTO @ids (Id) VALUES (1), (2), (3);
SELECT * FROM @ids;

Pegadinhas

Conclusão

In-Memory OLTP não é botão mágico de performance: ele brilha em cargas OLTP com muita concorrência e transações curtas, e cobra o preço em memória e em restrições de T-SQL. Para a DP-800, foque em quando usar, nos níveis de durabilidade e nos tipos de índice. No próximo post vamos voltar ao disco e falar de uma tabela que guarda o passado: as temporal tables.

-- Limpeza opcional desta parte
DROP PROCEDURE IF EXISTS dbo.usp_InserirPedido;
DROP TABLE IF EXISTS dbo.Pedido, dbo.SessaoCache;
-- O filegroup MEMORY_OPTIMIZED_DATA permanece no banco

Link para os scripts.

Sair da versão mobile