Treinamento SQL Server

Módulo 18 — Índices & Otimização de Consultas (Parte I)

Fundamentos de índices, SARGability, seeks vs scans, lookups e escolhas de chaves para consultas rápidas.

T-SQL Nível Intermediário/Avançado Hands-on Atualizado: 28 ago 2025

Objetivos

  • Diferenciar Heap, Clustered e Nonclustered.
  • Projetar chaves compostas e colunas INCLUDE para covering.
  • Entender SARGability e reduzir scans desnecessários.
  • Interpretar execution plans básicos e estatísticas de IO/tempo.

Pré-requisitos

  • Módulos 13–17 recomendados.
  • Conhecimentos de JOIN, GROUP BY e noções de estatísticas.
  • Acesso a um ambiente de testes.

1) Fundamentos de Índices

PK ≠ CI necessariamente: por padrão a PK cria um índice (unique) que pode ser o CI, mas você pode escolher outra coluna como clusterizada.

2) Tipos: Clustered, Nonclustered, Unique, Filtered

-- Clustered (escolha com parcimônia: estável, seletiva, usada em buscas/ordenações)
CREATE UNIQUE CLUSTERED INDEX CI_Pedidos ON dbo.Pedidos (IdPedido);

-- Nonclustered com INCLUDE (cobertura)
CREATE NONCLUSTERED INDEX IX_Pedidos_Cliente_Data
ON dbo.Pedidos (IdCliente, DataPedido)
INCLUDE (ValorTotal, Status);

-- Filtered Index (conjunto menor = menos custo de escrita/armazenamento)
CREATE NONCLUSTERED INDEX IX_Pedidos_Abertos
ON dbo.Pedidos (IdCliente)
WHERE Status = 'Aberto';
Filtered Index: ótimo para hot sets (ex.: ativos, recentes). Improva seletividade e reduz lookups.

3) Como a busca funciona: Seek, Scan, Lookup

-- Ex.: lookup por falta de cobertura
SELECT DataPedido, ValorTotal
FROM dbo.Pedidos
WHERE IdCliente = 123 AND Status = 'Pago';  -- se NCI cobre (IdCliente,Status) mas não as colunas selecionadas, haverá lookup
Covering Index: inclua colunas selecionadas/ordenadas como INCLUDE para eliminar lookups recorrentes.

4) Composição de chaves & INCLUDE

-- Cobrindo consulta: filtro por Cliente+Data, retorna Valor e Status
CREATE NONCLUSTERED INDEX IX_Pedidos_Cliente_Data_incl
ON dbo.Pedidos (IdCliente, DataPedido)
INCLUDE (ValorTotal, Status);

5) SARGability (filtros indexáveis)

Condições devem permitir que o otimizador navegue pela árvore do índice — evite funções na coluna, conversões implícitas, curingas iniciais etc.

-- Não SARGable (função em coluna)
WHERE YEAR(DataPedido) = 2025
-- SARGable (intervalo)
WHERE DataPedido >= '2025-01-01' AND DataPedido < '2026-01-01'
-- Conversão implícita (coluna varchar, parâmetro int) invalida seek
WHERE CPF = 12345678901    -- ERRADO se CPF é varchar
WHERE CPF = '12345678901'  -- CERTO (ou normalize o tipo)
-- LIKE com curinga inicial quebra seek
WHERE Nome LIKE '%Silva'   -- scan
WHERE Nome LIKE 'Silva%'   -- pode usar seek (índice por Nome)
OR vs IN: substitua col = a OR col = b por col IN (a,b). Para condições em colunas diferentes, considere UNION ALL ou indexes separados.

6) Planos de Execução & Métricas

-- Habilitar métricas
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO
-- Estimado vs Real (SSMS): Ctrl+L (Estimado), Ctrl+M (Real)

7) Manutenção (Fill Factor & Fragmentação)

-- Exemplo de rebuild com fill factor
ALTER INDEX IX_Pedidos_Cliente_Data_incl ON dbo.Pedidos
REBUILD WITH (FILLFACTOR = 90, ONLINE = ON);  -- ONLINE Enterprise

8) DMVs úteis (missing/usage)

-- Índices sugeridos pelo otimizador (use com cautela; avalie workload)
SELECT TOP 20
  migs.group_handle, mid.statement AS TableName,
  mid.equality_columns, mid.inequality_columns, mid.included_columns,
  migs.unique_compiles, migs.avg_total_user_cost, migs.avg_user_impact
FROM sys.dm_db_missing_index_group_stats AS migs
JOIN sys.dm_db_missing_index_groups AS mig ON migs.group_handle = mig.index_group_handle
JOIN sys.dm_db_missing_index_details AS mid ON mig.index_handle = mid.index_handle
ORDER BY migs.avg_user_impact DESC;

-- Uso de índices (para achar “não usados” ou “quentes”)
SELECT OBJECT_NAME(s.[object_id]) AS Tabela, i.name AS Indice,
       s.user_seeks, s.user_scans, s.user_lookups, s.user_updates
FROM sys.dm_db_index_usage_stats AS s
JOIN sys.indexes AS i ON i.[object_id]=s.[object_id] AND i.index_id=s.index_id
WHERE s.database_id = DB_ID()
ORDER BY (s.user_seeks + s.user_scans + s.user_lookups) DESC;
Atenção: DMVs “missing index” são resetadas no restart. Não crie índices cegamente; avalie sobreposição/impacto de escrita.

9) Laboratório Guiado

9.1) Setup

IF OBJECT_ID('dbo.Pedidos','U') IS NOT NULL DROP TABLE dbo.Pedidos;
GO
CREATE TABLE dbo.Pedidos(
  IdPedido     int IDENTITY PRIMARY KEY,
  IdCliente    int NOT NULL,
  DataPedido   datetime2(0) NOT NULL,
  Status       varchar(20) NOT NULL,
  ValorTotal   decimal(12,2) NOT NULL
);
GO
-- Índice de cobertura para consultas por cliente/data
CREATE NONCLUSTERED INDEX IX_Pedidos_Cliente_Data
ON dbo.Pedidos (IdCliente, DataPedido)
INCLUDE (ValorTotal, Status);
GO
-- Dados mínimos
INSERT INTO dbo.Pedidos (IdCliente, DataPedido, Status, ValorTotal)
SELECT v.IdCliente, v.DataVenda, 'Pago', v.Valor
FROM (VALUES
  (1, CAST('2025-08-01' AS date), 1200.00),
  (1, CAST('2025-08-15' AS date),  350.50),
  (2, CAST('2025-08-10' AS date),  999.90)
) AS v(IdCliente, DataVenda, Valor);

9.2) Seek vs Scan

-- Seek (SARGable)
SELECT ValorTotal
FROM dbo.Pedidos
WHERE IdCliente = 1 AND DataPedido >= '2025-08-01' AND DataPedido < '2025-09-01';

-- Scan (não SARGable por função)
SELECT ValorTotal
FROM dbo.Pedidos
WHERE IdCliente = 1 AND MONTH(DataPedido) = 8;  -- evitar

9.3) Eliminando Lookup com INCLUDE

-- Selecione Status também; se não estiver em INCLUDE, haverá Key Lookup
SELECT Status, ValorTotal
FROM dbo.Pedidos
WHERE IdCliente = 1 AND DataPedido >= '2025-08-01' AND DataPedido < '2025-09-01';

9.4) Índice filtrado para “abertos”

CREATE NONCLUSTERED INDEX IX_Pedidos_Abertos
ON dbo.Pedidos (IdCliente, DataPedido)
WHERE Status = 'Aberto';

9.5) Medindo IO/TEMPO

SET STATISTICS IO ON; SET STATISTICS TIME ON;
GO
SELECT SUM(ValorTotal)
FROM dbo.Pedidos
WHERE IdCliente = 1 AND DataPedido >= '2025-08-01' AND DataPedido < '2025-09-01';
GO
SET STATISTICS IO OFF; SET STATISTICS TIME OFF;

10) Exercícios

  1. Escolha de CI: Dada a tabela Pedidos, proponha uma chave clusterizada melhor que IdPedido (se aplicável) e justifique.
  2. Cobertura: Crie um NCI que elimine lookups na consulta por IdCliente+DataPedido que retorna ValorTotal e Status.
  3. Filtro de período: Reescreva um filtro não SARGable de mês/ano para intervalo >=/<.
  4. Filtered Index: Crie um índice apenas para Status='Pago' e mostre um exemplo que o use.
  5. DMVs: Liste os 5 índices mais usados e 5 menos usados no seu banco.

11) Gabarito (sugestões)

-- 1) CI sugerido (depende do workload; exemplo: DataPedido,IdPedido)
-- Útil se a maioria das consultas é por período cronológico
CREATE UNIQUE CLUSTERED INDEX CI_Pedidos_Data_Id
ON dbo.Pedidos (DataPedido, IdPedido);

-- 2) Cobertura (já criada no lab)
CREATE NONCLUSTERED INDEX IX_Pedidos_Cliente_Data
ON dbo.Pedidos (IdCliente, DataPedido)
INCLUDE (ValorTotal, Status);

-- 3) Filtro SARGable
WHERE DataPedido >= '2025-08-01' AND DataPedido < '2025-09-01';

-- 4) Filtered Index + uso
CREATE NONCLUSTERED INDEX IX_Pedidos_Pagos
ON dbo.Pedidos (IdCliente, DataPedido)
WHERE Status = 'Pago';
GO
SELECT SUM(ValorTotal)
FROM dbo.Pedidos WITH (INDEX(IX_Pedidos_Pagos))
WHERE Status = 'Pago' AND IdCliente = 1
  AND DataPedido >= '2025-08-01' AND DataPedido < '2025-09-01';

-- 5) DMVs (mais/menos usados)
SELECT TOP 5 OBJECT_NAME(s.[object_id]) AS Tabela, i.name AS Indice, s.*
FROM sys.dm_db_index_usage_stats AS s
JOIN sys.indexes AS i ON i.[object_id]=s.[object_id] AND i.index_id=s.index_id
WHERE s.database_id = DB_ID()
ORDER BY (s.user_seeks + s.user_scans + s.user_lookups) DESC;

SELECT TOP 5 OBJECT_NAME(i.[object_id]) AS Tabela, i.name AS Indice
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS s
  ON s.[object_id]=i.[object_id] AND s.index_id=i.index_id AND s.database_id = DB_ID()
WHERE i.is_hypothetical = 0 AND i.index_id > 0
ORDER BY COALESCE(s.user_seeks + s.user_scans + s.user_lookups, 0) ASC;

12) Erros Comuns & Checklist

Erros Comuns

  • Função na coluna filtrada (quebra seek).
  • Chave clusterizada volátil/longa (impacta todos NCIs).
  • Excesso de índices (custo de escrita e manutenção alto).
  • Ignorar cobertura e aceitar lookups caros em consultas críticas.

Checklist Rápido

  • Consulta crítica tem seek e está coberta?
  • Ordem e seletividade das colunas em chaves compostas corretas?
  • Filtered indexes considerados para subconjuntos quentes?
  • Plano real e métricas IO/TIME analisados?

13) Próximos Passos