Otimização de Consultas
Otimização de Consultas
💡 Analogia da Vida Real
Imagine que você está procurando um livro em uma biblioteca:
- Varredura completa da tabela: Procurar em todas as prateleiras da biblioteca →lento
- Índice: Primeiro verificar o catálogo de cartões, encontrar o número da prateleira e ir diretamente →rápido
- Plano de execução: O bibliotecário diz antecipadamente "pesquisar pelo índice do autor é mais rápido que pelo título" →EXPLAIN
- Log de consulta lenta: A biblioteca registra "livros que levaram mais de 10 minutos para encontrar" para otimização de prateleiras →log de consulta lenta
- Reescrita de consulta: Em vez de perguntar "todos os livros que contêm 'ciência'", primeiro reduzir por categoria e depois pesquisar precisamente →otimizar a abordagem de consulta
📖 Conceitos Principais
1. Plano de Execução EXPLAIN
EXPLAIN é a ferramenta central para otimização de consultas. Ele informa como o banco de dados executa uma instrução SQL.
EXPLAIN SELECT * FROM users WHERE username = 'alice';
Interpretação dos Campos Principais:
| Campo | Significado | Foco |
|---|---|---|
type |
Tipo de acesso | ALL(varredura completa) →index →range →ref →eq_ref →const, quanto mais à direita, melhor |
key |
Índice realmente usado | NULL significa que nenhum índice foi usado |
rows |
Linhas estimadas varridas | Quanto menor, melhor |
Extra |
Informações adicionais | Using filesort(ordenação necessária), Using temporary(tabela temporária necessária) merecem atenção |
possible_keys |
Índices potenciais | Ajuda a analisar se os índices estão sendo selecionados |
-- Ver plano de execução
EXPLAIN SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.name;
-- MySQL 8.0+ pode ver estatísticas reais de execução
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 100;
Tipos de Acesso Explicados:
Do pior ao melhor:
ALL → Varredura completa da tabela (deve otimizar)
index → Varredura completa do índice (levemente melhor que ALL)
range → Varredura de intervalo do índice (WHERE id > 100)
ref → Pesquisa de índice não único (WHERE username = 'alice')
eq_ref → Pesquisa de índice único (JOIN na chave primária)
const → Pesquisa constante (WHERE id = 1, mais rápido)
system → Tabela do sistema (raramente aparece)
2. Estratégias de Otimização de Índices
-- 1. Criar índices para campos frequentemente usados em WHERE, JOIN, ORDER BY
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_order_user_created ON orders(user_id, created_at);
-- 2. Índice de cobertura: campos da consulta estão todos no índice, sem necessidade de busca na tabela
-- Se executar frequentemente: SELECT id, user_id, created_at FROM orders WHERE user_id = ?
CREATE INDEX idx_order_covering ON orders(user_id, created_at, id);
-- 3. Índice de prefixo: indexar apenas os primeiros N caracteres de campos de string longos
CREATE INDEX idx_user_name_prefix ON users(username(10));
-- 4. Índice composto segue a regra do prefixo mais à esquerda
CREATE INDEX idx_abc ON table_name(a, b, c);
-- Pode combinar: WHERE a=1 | WHERE a=1 AND b=2 | WHERE a=1 AND b=2 AND c=3
-- Não pode combinar: WHERE b=2 | WHERE c=3 | WHERE b=2 AND c=3
-- 5. Ver uso de índices de uma tabela
SHOW INDEX FROM orders;
-- 6. Ver cardinalidade do índice (maior cardinalidade = melhor discriminação)
SELECT
INDEX_NAME,
COLUMN_NAME,
CARDINALITY
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_NAME = 'orders';
3. Evitando Varreduras Completas da Tabela
-- ✅Padrões que causam varreduras completas da tabela
-- 1. Usar funções em colunas indexadas
SELECT * FROM orders WHERE YEAR(created_at) = 2024;
-- ✅Reescrever como consulta de intervalo
SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
-- 2. Realizar aritmética em colunas indexadas
SELECT * FROM orders WHERE id + 1 = 100;
-- ✅Reescrever como
SELECT * FROM orders WHERE id = 99;
-- 3. Conversão implícita de tipo (phone é VARCHAR, passando INT)
SELECT * FROM users WHERE phone = 13800138000;
-- ✅Reescrever como
SELECT * FROM users WHERE phone = '13800138000';
-- 4. LIKE com curinga inicial
SELECT * FROM users WHERE name LIKE '%alice%';
-- ✅Se pesquisa fuzzy for necessária, considere índice de texto completo
ALTER TABLE users ADD FULLTEXT INDEX ft_name(name);
SELECT * FROM users WHERE MATCH(name) AGAINST('alice' IN BOOLEAN MODE);
-- 5. Condições OR podem causar falha no índice
SELECT * FROM users WHERE status = 1 OR age > 25;
-- ✅Usar UNION em vez disso
SELECT * FROM users WHERE status = 1
UNION
SELECT * FROM users WHERE age > 25;
-- 6. NOT IN / NOT EXISTS podem causar varredura completa da tabela
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders);
-- ✅Usar LEFT JOIN + IS NULL em vez disso
SELECT u.* FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL;
4. Técnicas de Reescrita de Consultas
-- 1. Usar EXISTS em vez de IN (mais eficiente para grandes conjuntos de dados)
-- Lento
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);
-- Rápido
SELECT * FROM users u WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 1000
);
-- 2. Usar UNION ALL em vez de UNION (quando deduplicação não é necessária)
-- UNION deduplica e ordena; UNION ALL não
SELECT name FROM users_2023 UNION ALL SELECT name FROM users_2024;
-- 3. Evitar SELECT *, selecionar apenas colunas necessárias
-- Lento
SELECT * FROM orders WHERE user_id = 1;
-- Rápido
SELECT id, order_no, total_amount, status FROM orders WHERE user_id = 1;
-- 4. Otimização de paginação (problema de paginação profunda)
-- Lento (OFFSET 100000 requer varredura de 100100 linhas)
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 100000;
-- Rápido (paginação por cursor, lembrar o último id da página anterior)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;
-- 5. Operações em lote em vez de operações em loop
-- Lento (inserções em loop)
INSERT INTO logs (msg) VALUES ('a');
INSERT INTO logs (msg) VALUES ('b');
INSERT INTO logs (msg) VALUES ('c');
-- Rápido (inserção em lote)
INSERT INTO logs (msg) VALUES ('a'), ('b'), ('c');
-- 6. Evitar usar <> ou != em colunas indexadas em cláusulas WHERE
SELECT * FROM users WHERE status != 0;
-- ✅Se houver apenas alguns valores de status, reescrever como
SELECT * FROM users WHERE status IN (1, 2, 3);
5. Log de Consulta Lenta
-- Ver configuração do log de consulta lenta
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- Habilitar log de consulta lenta
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- Registrar consultas que excedem 1 segundo
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- Registrar consultas que não usam índices
-- Analisar log de consulta lenta
-- Usando a ferramenta mysqldumpslow
-- mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
-- Usando pt-query-digest (Percona Toolkit)
-- pt-query-digest /var/log/mysql/slow.log
6. Lista de Verificação de Ajuste
| Etapa | Ação | Ferramenta |
|---|---|---|
| 1 | Habilitar log de consulta lenta, encontrar SQL lento | slow_query_log |
| 2 | Analisar plano de execução com EXPLAIN | EXPLAIN |
| 3 | Verificar se os índices estão sendo atingidos | Campo type |
| 4 | Verificar filesort/temporary | Campo Extra |
| 5 | Reescrever SQL ou adicionar índices | DDL / Reescrita SQL |
| 6 | Verificar resultados da otimização | Comparar tempos de execução |
7. Armadilhas Comuns de Performance
| Armadilha | Descrição | Solução |
|---|---|---|
| SELECT * | Seleciona todas as colunas, não pode usar índice de cobertura | Selecionar apenas colunas necessárias |
| Grande OFFSET | Paginação profunda varre grandes quantidades de dados | Paginação por cursor |
| N+1 consultas | Consultar dados relacionados um por um em um loop | Consulta em lote ou JOIN |
| JOIN sem índices | Junções de tabelas grandes sem índices | Adicionar índices em colunas de JOIN |
| Grandes transações | Manter locks por muito tempo | Reduzir escopo da transação |
| Tipo de dado errado | Usar VARCHAR para números | Escolher tipos apropriados |
💡 Sintaxe Básica
-- Uso básico do EXPLAIN
EXPLAIN SELECT ...;
EXPLAIN ANALYZE SELECT ...; -- MySQL 8.0+
-- Ver tempo de execução da consulta
SET profiling = 1;
SELECT ...;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
-- Ver índices
SHOW INDEX FROM table_name;
-- Forçar uso de um índice específico
SELECT * FROM orders FORCE INDEX (idx_user_id) WHERE user_id = 100;
-- Ignorar índice (para testes de comparação)
SELECT * FROM orders IGNORE INDEX (idx_user_id) WHERE user_id = 100;
EXPLAIN é seu melhor amigo.
Comparação de Dialectos de Banco de Dados
Diferentes bancos de dados têm diferenças significativas de sintaxe. Aqui está uma comparação de operações comuns:
Paginação com LIMIT
-- MySQL / PostgreSQL / SQLite
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 20;
-- SQL Server
SELECT * FROM orders ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- Antes do SQL Server 2012
SELECT TOP 10 * FROM orders WHERE id NOT IN (SELECT TOP 20 id FROM orders ORDER BY id);
-- Oracle 12c+
SELECT * FROM orders ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- Antes do Oracle 12c
SELECT * FROM (SELECT o.*, ROWNUM rn FROM orders o WHERE ROWNUM <= 30) WHERE rn > 20;
Chave Primária Auto-Incremento
-- MySQL
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50));
-- Ou usar valor padrão
INSERT INTO users (name) VALUES ('Alice'); -- id gerado automaticamente
-- PostgreSQL
CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(50));
-- PostgreSQL 10+
CREATE TABLE users (id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR(50));
-- SQLite
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);
-- SQLite também suporta ROWID
INSERT INTO users (name) VALUES ('Alice');
-- SQL Server
CREATE TABLE users (id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50));
-- Oracle
CREATE TABLE users (id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR2(50));
Funções de String
-- Concatenação de string
SELECT CONCAT('Hello', ' ', 'World'); -- MySQL, PostgreSQL
SELECT 'Hello' || ' ' || 'World'; -- PostgreSQL, SQLite, Oracle
SELECT name + ' ' + email FROM users; -- SQL Server
-- Substring
SELECT SUBSTRING('Hello World', 1, 5); -- MySQL, SQL Server
SELECT SUBSTR('Hello World', 1, 5); -- PostgreSQL, SQLite, Oracle
-- Comprimento da string
SELECT LENGTH('Hello'); -- MySQL, PostgreSQL, SQLite
SELECT LEN('Hello'); -- SQL Server
-- Conversão de maiúsculas/minúsculas
SELECT UPPER('hello'), LOWER('HELLO'); -- Todos os bancos de dados
SELECT UCASE('hello'), LCASE('HELLO'); -- MySQL suporta adicionalmente
-- Remover espaços em branco
SELECT TRIM(' Hello '); -- Todos os bancos de dados
SELECT LTRIM(' Hello'), RTRIM('Hello '); -- MySQL, SQL Server, PostgreSQL
-- Substituir
SELECT REPLACE('Hello World', 'World', 'SQL'); -- Todos os bancos de dados
Funções de Data
-- Hora atual
SELECT NOW(); -- MySQL, PostgreSQL
SELECT CURRENT_TIMESTAMP; -- Todos os bancos de dados
SELECT GETDATE(); -- SQL Server
SELECT datetime('now'); -- SQLite
-- Aritmética de data
SELECT DATE_ADD('2024-01-01', INTERVAL 30 DAY); -- MySQL
SELECT '2024-01-01'::DATE + INTERVAL '30 days'; -- PostgreSQL
SELECT DATEADD(DAY, 30, '2024-01-01'); -- SQL Server
SELECT date('2024-01-01', '+30 days'); -- SQLite
-- Extrair ano/mês/dia
SELECT YEAR(created_at), MONTH(created_at), DAY(created_at) FROM orders; -- MySQL, SQL Server
SELECT EXTRACT(YEAR FROM created_at) FROM orders; -- PostgreSQL, MySQL 8.0+
SELECT strftime('%Y', created_at) FROM orders; -- SQLite
-- Formatação de data
SELECT DATE_FORMAT(created_at, '%Y-%m-%d') FROM orders; -- MySQL
SELECT TO_CHAR(created_at, 'YYYY-MM-DD') FROM orders; -- PostgreSQL, Oracle
SELECT FORMAT(created_at, 'yyyy-MM-dd') FROM orders; -- SQL Server
SELECT strftime('%Y-%m-%d', created_at) FROM orders; -- SQLite
Expressões Condicionais
-- Expressão IF
SELECT IF(score >= 60, 'Aprovado', 'Reprovado') FROM exams; -- MySQL
SELECT IIF(score >= 60, 'Aprovado', 'Reprovado') FROM exams; -- SQL Server
SELECT CASE WHEN score >= 60 THEN 'Aprovado' ELSE 'Reprovado' END FROM exams; -- Todos os bancos de dados
-- COALESCE (retorna o primeiro valor não-NULL)
SELECT COALESCE(nickname, username, 'Anônimo') FROM users; -- Todos os bancos de dados
-- NULLIF (retorna NULL quando iguais)
SELECT NULLIF(a, b); -- Todos os bancos de dados
Tipo Booleano
-- MySQL: Sem BOOLEAN nativo, use TINYINT(1) em vez disso
CREATE TABLE users (is_active TINYINT(1) DEFAULT 1);
SELECT * FROM users WHERE is_active = TRUE; -- TRUE equivale a 1
-- PostgreSQL: BOOLEAN nativo
CREATE TABLE users (is_active BOOLEAN DEFAULT TRUE);
SELECT * FROM users WHERE is_active = TRUE;
-- SQLite: Sem BOOLEAN nativo, use INTEGER
CREATE TABLE users (is_active INTEGER DEFAULT 1);
SELECT * FROM users WHERE is_active = 1;
-- SQL Server: BIT nativo
CREATE TABLE users (is_active BIT DEFAULT 1);
SELECT * FROM users WHERE is_active = 1;
UPSERT (Atualizar se existir, inserir se não)
-- MySQL
INSERT INTO stats (article_id, view_count) VALUES (1, 1)
ON DUPLICATE KEY UPDATE view_count = view_count + 1;
-- PostgreSQL
INSERT INTO stats (article_id, view_count) VALUES (1, 1)
ON CONFLICT (article_id) DO UPDATE SET view_count = stats.view_count + 1;
-- SQLite
INSERT INTO stats (article_id, view_count) VALUES (1, 1)
ON CONFLICT(article_id) DO UPDATE SET view_count = view_count + 1;
-- SQL Server
MERGE INTO stats AS target
USING (SELECT 1 AS article_id, 1 AS view_count) AS source
ON target.article_id = source.article_id
WHEN MATCHED THEN UPDATE SET view_count = target.view_count + 1
WHEN NOT MATCHED THEN INSERT (article_id, view_count) VALUES (source.article_id, source.view_count);
Suporte a Funções de Janela
-- Todos os principais bancos de dados as suportam (MySQL 8.0+, PostgreSQL, SQL Server, SQLite 3.25+)
SELECT
name,
score,
RANK() OVER (ORDER BY score DESC) AS ranking,
ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) AS class_rank
FROM students;
-- MySQL 5.7 e abaixo não suportam funções de janela; use variáveis para simular
-- GROUP_CONCAT / STRING_AGG
SELECT category_id, GROUP_CONCAT(name SEPARATOR ',') FROM products GROUP BY category_id; -- MySQL
SELECT category_id, STRING_AGG(name, ',') FROM products GROUP BY category_id; -- PostgreSQL
Exemplo: Localizar e Otimizar uma Consulta Lenta (Dificuldade 🔥
Consulta lenta original:
-- Consultar valor total de pedidos por usuário nos últimos 30 dias (assumir tabela users com 100K linhas, tabela orders com 1M linhas)
EXPLAIN
SELECT u.name, u.email, SUM(o.total_amount) AS total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.name, u.email
ORDER BY total_spent DESC
LIMIT 20;
Análise do EXPLAIN revela:
type: ALL(varredura completa da tabela orders)rows: 1000000(varrendo milhões de linhas)Extra: Using temporary; Using filesort
Etapas de otimização:
-- 1. Adicionar índice composto na tabela orders
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);
-- 2. Otimizar consulta: filtrar primeiro, depois juntar
SELECT u.name, u.email, sub.total_spent
FROM users u
INNER JOIN (
SELECT user_id, SUM(total_amount) AS total_spent
FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY user_id
ORDER BY total_spent DESC
LIMIT 20
) sub ON u.id = sub.user_id
ORDER BY sub.total_spent DESC;
-- 3. EXPLAIN novamente para confirmar efeito da otimização
EXPLAIN SELECT u.name, u.email, sub.total_spent ...
-- type: ref, rows: significativamente reduzido
Exemplo: Otimização de Paginação Profunda (Dificuldade ⭐⭐)
Consulta problemática:
-- Consultar página 10000 (20 por página), OFFSET 200000
SELECT id, title, created_at
FROM articles
WHERE status = 1
ORDER BY created_at DESC
LIMIT 20 OFFSET 200000;
-- Mesmo com um índice, requer varredura de 200020 linhas, muito lento
Solução 1: Paginação por cursor (recomendada)
-- Lembrar o último created_at e id da página anterior
-- Assumir último item da página anterior: created_at='2024-03-15 10:30:00', id=50001
SELECT id, title, created_at
FROM articles
WHERE status = 1
AND (created_at < '2024-03-15 10:30:00'
OR (created_at = '2024-03-15 10:30:00' AND id < 50001))
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- Varre apenas 20 linhas, extremamente rápido
Solução 2: Join adiado
-- Primeiro consultar chaves primárias, depois juntar para dados completos
SELECT a.id, a.title, a.created_at
FROM articles a
INNER JOIN (
SELECT id FROM articles
WHERE status = 1
ORDER BY created_at DESC
LIMIT 20 OFFSET 200000
) b ON a.id = b.id;
-- Subconsulta usa índice de cobertura; consulta principal busca dados por chave primária
Solução 3: Otimização na camada de negócio
-- Se permitido, limitar profundidade máxima de paginação
-- Permitir apenas visualização dos primeiros 1000 resultados, solicitar que os usuários usem pesquisa para reduzir
SELECT id, title, created_at
FROM articles
WHERE status = 1
AND category_id = 5 -- Adicionar condições de filtro
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;
🔧 Cenário 1: Otimização de Consulta de Lista de Produtos de E-commerce
-- Consulta original: filtro multi-condição + ordenação + paginação
SELECT p.id, p.name, p.price, p.sales_count, c.name AS category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.status = 1
AND p.category_id IN (10, 11, 12, 13)
AND p.price BETWEEN 50 AND 500
ORDER BY p.sales_count DESC
LIMIT 20;
-- Otimização:
-- 1. Criar índice composto cobrindo principais condições de filtro
CREATE INDEX idx_product_filter ON products(status, category_id, price, sales_count);
-- 2. Se EXPLAIN mostrar filesort, ajustar ordem do índice
CREATE INDEX idx_product_sort ON products(status, category_id, sales_count DESC, price);
-- 3. Se a tabela categories for pequena, desnormalizar nome da categoria na tabela products
ALTER TABLE products ADD COLUMN category_name VARCHAR(50);
-- Manter durante INSERT/UPDATE
🔧 Cenário 2: Otimização de Consulta de Relatório Estatístico
-- Consulta original: estatísticas mensais de pedidos (conjunto de dados grande, lento toda vez)
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS order_count,
SUM(total_amount) AS revenue
FROM orders
WHERE created_at >= '2023-01-01'
GROUP BY month
ORDER BY month;
-- Opção de otimização 1: Tabela de resumo pré-computada
CREATE TABLE monthly_order_stats (
month_key VARCHAR(7) PRIMARY KEY COMMENT 'Formato: 2024-01',
order_count INT NOT NULL DEFAULT 0,
revenue DECIMAL(15,2) NOT NULL DEFAULT 0,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Trabalho agendado atualiza diariamente (cálculo incremental)
INSERT INTO monthly_order_stats (month_key, order_count, revenue)
SELECT
DATE_FORMAT(created_at, '%Y-%m'),
COUNT(*),
SUM(total_amount)
FROM orders
WHERE created_at >= CURDATE() - INTERVAL 1 DAY
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ON DUPLICATE KEY UPDATE
order_count = VALUES(order_count),
revenue = VALUES(revenue),
updated_at = NOW();
-- Consultar tabela de resumo (resposta em milissegundos)
SELECT * FROM monthly_order_stats ORDER BY month_key;
❓ Perguntas Frequentes
P: Quanto mais índices, melhor? R: Não. Índices consomem espaço de armazenamento e desaceleram operações INSERT/UPDATE/DELETE (cada operação de escrita deve atualizar índices). Crie índices apenas para consultas que realmente precisam de aceleração, e limpe regularmente índices não utilizados.
P: Como devo escolher a ordem dos campos para índices compostos? R: Coloque campos de alta cardinalidade primeiro (ex.: user_id tem maior cardinalidade que status); coloque campos de consulta de igualdade antes de campos de consulta de intervalo; decida com base nas combinações reais de condições de consulta.
P: Minha consulta já é rápida, devo ainda otimizar? R: Se o tempo de resposta estiver dentro da faixa aceitável, a otimização excessiva não é necessária. Mas esteja ciente de que a performance pode degradar à medida que os dados cresçam; planeje antecipadamente com testes de carga e estratégia de índices.
P: As linhas mostradas pelo EXPLAIN são precisas? R: Não totalmente precisas; são estimativas baseadas em estatísticas. A execução real pode varrer mais ou menos linhas. Executar
ANALYZE TABLEpara atualizar estatísticas pode melhorar a precisão das estimativas.
📖 Resumo
Esta lição cobriu sistematicamente métodos de otimização de consultas SQL:
- EXPLAIN é a ferramenta central para analisar planos de execução; foque em
type,key,rows,Extra - Otimização de índices: Criar índices com sabedoria, seguir a regra do prefixo mais à esquerda, usar índices de cobertura
- Evitar varreduras completas da tabela: Não usar funções/aritmética em colunas indexadas, atentar para conversões implícitas de tipo, usar LIKE %xxx% com cautela
- Reescrita de consultas: EXISTS em vez de IN, paginação por cursor em vez de grande OFFSET, operações em lote em vez de loops
- Logs de consultas lentas: Identificar SQL gargalo para otimização direcionada
- Dialectos de bancos de dados: MySQL/PostgreSQL/SQLite/SQL Server têm diferenças significativas de sintaxe; esteja atento ao escrever SQL multi-banco
📝 Exercícios
- Execute análise EXPLAIN na seguinte SQL e otimize-a:SQL
SELECT * FROM orders WHERE YEAR(created_at) = 2024 AND user_id IN (SELECT id FROM users WHERE status = 1); - Projete uma estratégia de índice adequada para uma página de lista de artigos (suportando filtro por categoria, ordenação por tempo e paginação).
- Reescreva uma consulta usando
LIMIT 20 OFFSET 100000para usar paginação por cursor.
Próxima Lição →28-project.md



