Voltar ao portfólio
SQL Lab

Otimização de consulta SQL Server

Estudo de caso focado em identificar gargalos de performance, analisar a causa do problema e reestruturar uma consulta para melhorar sua eficiência.

01

Pergunta de negócio

Como identificar os clientes que mais geraram receita nos últimos 90 dias sem comprometer o tempo de resposta do dashboard?

02

Contexto do problema

Uma consulta utilizada para alimentar um relatório comercial apresentava aumento progressivo no tempo de execução conforme o volume de vendas crescia.

O objetivo da investigação foi identificar quais operações estavam causando maior custo ao SQL Server e reduzir o processamento sem alterar o resultado esperado pelo negócio.

03

Query original

Consulta utilizada antes da otimização.

Problema
SQL
SELECT
    c.CustomerKey,      -- Identifica cada cliente de forma única
    c.CustomerName,     -- Exibe o nome do cliente no resultado
    c.Segment,          -- Permite analisar o cliente pelo segmento
    SUM(s.TotalAmount) AS Revenue90Days, -- Soma a receita gerada nos últimos 90 dias
    COUNT(s.SaleKey) AS SalesCount       -- Conta quantas vendas o cliente realizou
FROM gold.DimCustomer c
INNER JOIN gold.FactSales s
    ON s.CustomerKey = c.CustomerKey     -- Relaciona cada venda ao respectivo cliente
WHERE
    CONVERT(DATE, s.SaleDate) >=
        CONVERT(DATE, DATEADD(DAY, -90, GETDATE()))
        -- Mantém somente vendas dos últimos 90 dias.
        -- O CONVERT na coluna SaleDate será um dos pontos avaliados na otimização.

    AND UPPER(s.Status) = 'APPROVED'
        -- Considera apenas vendas aprovadas.
        -- O UPPER aplicado na coluna também será avaliado por adicionar processamento.
GROUP BY
    c.CustomerKey,
    c.CustomerName,
    c.Segment           -- Agrupa as vendas para calcular os totais por cliente
ORDER BY
    Revenue90Days DESC; -- Mostra primeiro os clientes com maior receita
04

Investigação

Antes de alterar a consulta, o primeiro passo foi medir seu comportamento utilizando as estatísticas do SQL Server, devemos ligar e desligar as estatisticas como abaixo:

SQL Diagnóstico
-- Mostra quantas páginas de dados foram lidas durante a consulta
SET STATISTICS IO ON;

-- Mostra o tempo de CPU e o tempo total de execução
SET STATISTICS TIME ON;

-- Executar aqui a consulta original para medir seu comportamento

-- Desliga as estatísticas após o teste
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;

Após executar a consulta com STATISTICS IO e STATISTICS TIME habilitados, o SQL Server apresenta na aba "Mensagens" métricas que ajudam a avaliar o custo da consulta. Rows Returned representa a quantidade de linhas retornadas pelo resultado; Logical Reads indica quantas páginas de dados o SQL Server precisou acessar na memória para processar a consulta; e Execution Time mostra o tempo gasto pelo processador (CPU Time) e o tempo total decorrido (Elapsed Time). Os tempos são apresentados em milissegundos (ms).

Rows Returned

A consulta original retornou 16.449 linhas no conjunto analisado.

Logical Reads

FactSales: 6.618 leituras lógicas e DimCustomer: 279 leituras lógicas.

Execution Time

CPU: 484 ms
Tempo decorrido: 796 ms

05

Problemas encontrados

01

CONVERT

O uso de CONVERT diretamente sobre SaleDate dificulta o aproveitamento eficiente de índices existentes.

02

UPPER

Aplicar UPPER durante o filtro adiciona processamento e pode prejudicar estratégias eficientes de busca.

03

ÍNDICE

A consulta dependia de colunas frequentemente utilizadas nos filtros, relacionamento e agregação.

06

Ajuste - INDEX

Antes de propor um novo índice, o primeiro passo foi verificar os índices já existentes em gold.FactSales. Isso evita estruturas redundantes e permite avaliar se algum índice atual já atende às colunas utilizadas pela consulta.

SQL
SELECT
    i.name AS IndexName,                       -- Nome do índice encontrado
    i.type_desc AS IndexType,                  -- Informa se é clustered ou nonclustered
    i.is_unique AS IsUnique,                   -- Mostra se o índice exige valores únicos
    c.name AS ColumnName,                      -- Coluna que faz parte do índice
    ic.key_ordinal AS KeyOrder,                -- Ordem da coluna dentro da chave do índice
    ic.is_included_column AS IsIncludedColumn  -- Indica se a coluna está apenas no INCLUDE
FROM sys.indexes i
INNER JOIN sys.index_columns ic
    ON i.object_id = ic.object_id
    AND i.index_id = ic.index_id               -- Liga o índice às suas colunas
INNER JOIN sys.columns c
    ON ic.object_id = c.object_id
    AND ic.column_id = c.column_id             -- Recupera o nome de cada coluna
WHERE
    i.object_id = OBJECT_ID('gold.FactSales')  -- Analisa somente a tabela usada no teste
    AND i.is_hypothetical = 0                  -- Ignora índices hipotéticos do otimizador
ORDER BY
    i.name,
    ic.is_included_column,
    ic.key_ordinal,
    ic.index_column_id;                        -- Organiza o resultado para facilitar a leitura

Caso a verificação confirme que não existe um índice adequado para o padrão de filtro e relacionamento da consulta, a seguinte estratégia pode então ser testada.

SQL Índice analisado
CREATE NONCLUSTERED INDEX IX_FactSales_Status_SaleDate_CustomerKey
ON gold.FactSales (
    Status,       -- Usado no filtro para manter somente vendas APPROVED
    SaleDate,     -- Usado no filtro dos últimos 90 dias
    CustomerKey   -- Usado no relacionamento com DimCustomer
)
INCLUDE (
    TotalAmount   -- Necessário para calcular a receita sem buscar a coluna fora do índice
);
06

Ajuste - UPPER, CONVERT

Reestruturação da consulta, monitorando a performance.

Solução
SQL
-- Ativa as métricas para comparar a nova versão com a consulta original
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO

-- Calcula a data inicial uma única vez, fora do filtro da tabela
DECLARE @StartDate DATE;

SET @StartDate = DATEADD(DAY, -90, GETDATE());

SELECT
    c.CustomerKey,      -- Identifica cada cliente de forma única
    c.CustomerName,     -- Exibe o nome do cliente no resultado
    c.Segment,          -- Mantém o segmento para análise
    SUM(s.TotalAmount) AS Revenue90Days, -- Soma a receita dos últimos 90 dias
    COUNT(*) AS SalesCount               -- Conta as vendas encontradas para cada cliente
FROM gold.FactSales s
INNER JOIN gold.DimCustomer c
    ON c.CustomerKey = s.CustomerKey     -- Relaciona a venda ao cliente
WHERE
    s.SaleDate >= @StartDate
        -- Filtra diretamente pela coluna SaleDate, sem aplicar CONVERT nela

    AND s.Status = 'APPROVED'
        -- Filtra diretamente pelo status, sem aplicar UPPER na coluna
GROUP BY
    c.CustomerKey,
    c.CustomerName,
    c.Segment           -- Consolida receita e quantidade de vendas por cliente
ORDER BY
    Revenue90Days DESC; -- Prioriza os clientes que mais geraram receita
GO

-- Desliga as métricas após o teste
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO

Rows Returned

A versão otimizada retornou 16.449 linhas no teste realizado.

Logical Reads

FactSales: 477 leituras lógicas e DimCustomer: 279 leituras lógicas.

Execution Time

CPU: 125 ms
Tempo decorrido: 286 ms

07

Resultado

Foram comparados tempo decorrido, CPU e leituras lógicas no mesmo ambiente de testes. As duas versões retornaram 16.449 linhas, preservando o mesmo resultado para a comparação.

Antes 796 ms

Query original

Depois 286 ms

Query otimizada

Melhoria 64,1%

Redução do tempo decorrido

FactSales Reads

6.618 → 477
Redução de 92,8%.

CPU Time

484 ms → 125 ms
Redução de 74,2%.

Elapsed Time

796 ms → 286 ms
Redução de 64,1%.

08

Conclusão

Os testes mostraram que ajustes simples podem fazer bastante diferença. A consulta caiu de 796 ms para 286 ms e as leituras da FactSales foram de 6.618 para 477.

Mais importante do que apenas olhar o tempo foi medir o que o SQL Server estava lendo e processando. Também vale conferir os índices existentes antes de criar um novo. Assim, a otimização deixa de ser tentativa e erro e passa a ser uma decisão baseada nos resultados do próprio banco.

SARGability Execution Plan Indexing STATISTICS IO Performance Tuning
Voltar ao portfólio