Ideia central
Query tuning é o processo de identificar e corrigir queries que consomem recursos excessivos (CPU, I/O, memória, locks). O ciclo básico é: medir → identificar gargalo → entender o plano de execução → intervir → validar melhora.
1. Identificar as queries problemáticas
Antes de otimizar qualquer coisa, meça. As DMVs mostram o que está custando mais:
-- Top 10 queries por CPU acumulado
SELECT TOP 10
qs.total_worker_time / qs.execution_count AS avg_cpu_us,
qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
qs.total_elapsed_time / qs.execution_count AS avg_duration_us,
qs.execution_count,
SUBSTRING(st.text,
(qs.statement_start_offset / 2) + 1,
((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) + 1
) AS query_text,
qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
ORDER BY qs.total_worker_time DESC;2. Ler o Plano de Execução
O plano de execução mostra como o SQL Server vai (ou foi) executar a query, sendo o diagnóstico mais importante no tuning.
Onde ver:
Ctrl+Lno SSMS: plano estimado (sem executar)Ctrl+Mno SSMS: habilita plano real ao executar- Clicar com botão direito em uma query no plan cache → “View Execution Plan”
O que procurar:
| Sinal de alerta no plano | O que pode indicar |
|---|---|
| Table Scan / Clustered Index Scan | Falta de índice ou query não-SARGable |
| Key Lookup | Índice non-clustered incompleto (falta INCLUDE) |
| Hash Match | Dados não ordenados, pode ser substituído por Merge Join com índice correto |
| Sort | Dados chegando fora de ordem, índice pode eliminar |
| Seta larga (muitas linhas) | Estimativa incorreta ou muito dado sendo processado |
| Aviso amarelo no operador | Implicit conversion, falta de estatísticas, spill para disco |
| Número estimado ≠ real | Estatísticas desatualizadas ou parâmetros com sniffing ruim |
3. Implicit Conversion: armadilha silenciosa
Comparar colunas com tipos diferentes força o SQL Server a converter os dados, impedindo uso de índices:
-- Ruim: ClienteId é INT, mas passamos string → implicit conversion
WHERE ClienteId = '12345'
-- Bom: tipos compatíveis
WHERE ClienteId = 12345
-- Ruim: coluna VARCHAR com parâmetro NVARCHAR
WHERE codigo = N'ABC' -- N'' força NVARCHAR
-- Verificar implicit conversions no plano: aviso "Type conversion in expression may affect CardinalityEstimate"4. SARGability: deixar os índices trabalharem
Uma condição é SARGable (Search ARGument Able) quando o SQL Server consegue usar um índice range scan para avaliá-la.
-- NÃO-SARGable: função aplicada na coluna
WHERE YEAR(data_pedido) = 2026
WHERE CAST(id AS VARCHAR) = '100'
WHERE LTRIM(nome) = 'Logan'
-- SARGable: coluna nua, comparação direta
WHERE data_pedido >= '2026-01-01' AND data_pedido < '2027-01-01'
WHERE id = 100
WHERE nome = 'Logan'
-- NÃO-SARGable: negação com LIKE no início
WHERE produto LIKE '%notebook%'
-- SARGable: LIKE com sufixo
WHERE produto LIKE 'notebook%'5. Estratégias de indexação
Composite index: ordem das colunas importa:
Coloque primeiro as colunas de igualdade (=), depois as de range (>, <, BETWEEN):
-- Query: WHERE status = 'ativo' AND data_criacao > '2026-01-01'
-- Índice ideal: status primeiro (igualdade), depois data_criacao (range)
CREATE INDEX idx_status_data ON dbo.pedidos (status, data_criacao);Covering index com INCLUDE:
-- Query: SELECT id, total, data FROM pedidos WHERE cliente_id = 100
-- Sem INCLUDE: SQL Server faz Key Lookup na tabela para buscar total e data
-- Com INCLUDE: todas as colunas necessárias estão no índice → sem Key Lookup
CREATE INDEX idx_pedidos_cliente
ON dbo.pedidos (cliente_id)
INCLUDE (id, total, data);Filtered index: índice parcial:
-- Índice apenas para pedidos pendentes (subconjunto menor, muito mais eficiente)
CREATE INDEX idx_pedidos_pendentes
ON dbo.pedidos (data_criacao, cliente_id)
WHERE status = 'pendente';6. Parameter Sniffing
O SQL Server compila stored procedures com o plano otimizado para os primeiros parâmetros usados. Se os próximos parâmetros têm distribuição muito diferente, o plano pode ser ruim.
-- Diagnóstico: ver quantas vezes o plano foi recompilado e os parâmetros
SELECT qs.execution_count, qp.query_plan
FROM sys.dm_exec_procedure_stats ps
CROSS APPLY sys.dm_exec_query_plan(ps.plan_handle) qp
WHERE OBJECT_NAME(ps.object_id) = 'usp_BuscarPedidos';
-- Soluções:
-- 1. OPTION (RECOMPILE): recompila para cada execução (custo de CPU, mas plano sempre otimizado)
SELECT * FROM pedidos WHERE cliente_id = @cliente_id
OPTION (RECOMPILE);
-- 2. OPTIMIZE FOR: diz ao otimizador para usar um valor específico (ou UNKNOWN)
SELECT * FROM pedidos WHERE cliente_id = @cliente_id
OPTION (OPTIMIZE FOR (@cliente_id = 100));
OPTION (OPTIMIZE FOR UNKNOWN); -- usa média estatística, não o valor atual
-- 3. Variável local (quebra o sniffing, mas pode gerar plano genérico)
DECLARE @local_id INT = @cliente_id;
SELECT * FROM pedidos WHERE cliente_id = @local_id;7. Estatísticas
O otimizador usa estatísticas para estimar a cardinalidade (quantas linhas retornar). Estatísticas desatualizadas geram planos ruins.
-- Ver quando estatísticas foram atualizadas
SELECT
OBJECT_NAME(s.object_id) AS tabela,
s.name AS estatistica,
sp.last_updated,
sp.rows,
sp.rows_sampled,
sp.modification_counter
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE OBJECT_NAME(s.object_id) = 'pedidos'
ORDER BY sp.last_updated;
-- Atualizar com scan completo (mais preciso, mais lento)
UPDATE STATISTICS dbo.pedidos WITH FULLSCAN;
-- Habilitar atualização automática de estatísticas (deve estar ON)
ALTER DATABASE MinhaDB SET AUTO_UPDATE_STATISTICS ON;
ALTER DATABASE MinhaDB SET AUTO_UPDATE_STATISTICS_ASYNC ON; -- não bloqueia queries8. CTEs vs Tabelas Temporárias vs Table Variables
-- CTE: elegante, mas o SQL Server pode não materializar
-- O otimizador pode expandir inline, bom ou ruim dependendo do caso
WITH pedidos_recentes AS (
SELECT * FROM pedidos WHERE data > DATEADD(DAY, -30, GETDATE())
)
SELECT * FROM pedidos_recentes WHERE status = 'pendente';
-- Tabela temporária: materializa em TempDB, tem estatísticas
-- Use quando a CTE é consultada mais de uma vez ou tem muitas linhas
CREATE TABLE #pedidos_recentes (
id INT, cliente_id INT, total DECIMAL(18,2), status VARCHAR(20)
);
INSERT INTO #pedidos_recentes
SELECT id, cliente_id, total, status FROM pedidos
WHERE data > DATEADD(DAY, -30, GETDATE());
CREATE INDEX idx_temp_status ON #pedidos_recentes(status); -- pode indexar!
SELECT * FROM #pedidos_recentes WHERE status = 'pendente';
DROP TABLE #pedidos_recentes;
-- Table variable (@tabela): sem estatísticas, sem índices úteis
-- Use apenas para conjuntos muito pequenos (< 100 linhas)9. Checklist de tuning
- Plano de execução analisado (scans, lookups, spills)?
- A query é SARGable (sem funções nas colunas filtradas)?
- Tipos de dados compatíveis (sem implicit conversion)?
- Índices adequados existem (teste com
sp_BlitzIndexou missing index DMV)? - Estatísticas atualizadas?
- Parameter sniffing avaliado para SPs?
-
SELECT *eliminado? - Transações o mais curtas possível?
- TempDB não está sendo gargalo (sort spills, version store)?
Conexões
- sqlserver-visao-geral - Arquitetura interna (Buffer Pool, como queries são processadas)
- sqlserver-boas-praticas - Práticas de escrita T-SQL que evitam problemas de performance
- sqlserver-ferramentas - DMVs, sp_BlitzCache, Extended Events para diagnóstico
- sqlserver-administracao - Manutenção de índices e estatísticas
Referências
- Use The Index, Luke, melhor recurso gratuito sobre índices SQL
- Brent Ozar - How to Think Like the SQL Server Engine
- SQL Server Execution Plans - Grant Fritchey
- Parameter Sniffing - Kendra Little