Design de Banco de Dados
Design de Banco de Dados
🎯 Analogia da Vida Real
Imagine que você está abrindo uma biblioteca:
- Análise de Requisitos: Primeiro descobrir quais livros emprestar, quem os empresta e como registrá-los → entender os requisitos do negócio
- Diagrama ER: Desenhar um diagrama de relacionamento de "leitores — empréstimo — livros" → modelagem entidade-relacionamento
- Normalização: Não copiar endereços dos leitores repetidamente nos cartões de biblioteca; armazenar o endereço apenas no arquivo do leitor → eliminar redundância
- Desnormalização: Calcular os rankings de livros populares e afixá-los diretamente na parede, em vez de pesquisar nos registros de empréstimo toda vez → trocar espaço por tempo
- Convenção de Nomenclatura: Usar um formato unificado de número de prateleira como "A-01", não às vezes "Prateleira 1" e às vezes "Fileira 1" → nomenclatura unificada
📚 Conceitos Principais
1. Análise de Requisitos
Antes de escrever qualquer SQL, responda a estas perguntas:
| Pergunta | Exemplo |
|---|---|
| Quais entidades o sistema gerencia? | Usuários, artigos, comentários, tags |
| Quais são os relacionamentos entre entidades? | Um usuário escreve muitos artigos (um-para-muitos) |
| Quais consultas precisam ser suportadas? | Pesquisa por tag, ordenação por tempo |
| Volume estimado de dados? | Milhões de artigos, dezenas de milhões de comentários |
| Leitura intensiva ou escrita intensiva? | Sistemas de blog são de leitura intensiva |
2. Diagrama ER (Diagrama Entidade-Relacionamento)
Diagramas ER descrevem modelos de dados usando três elementos principais:
[Entidade] —— Atributo1, Atributo2, Atributo3
|
(Relacionamento) cardinalidade: 1:1, 1:N, M:N
|
[Entidade] —— Atributo1, Atributo2
Tipos Comuns de Relacionamento:
| Relacionamento | Exemplo | Implementação |
|---|---|---|
| Um-para-Um (1:1) | Usuário ↔ Detalhes do Usuário | Chave estrangeira ou mesclar em uma tabela |
| Um-para-Muitos (1:N) | Usuário → Artigo | Adicionar chave estrangeira user_id na tabela de artigos |
| Muitos-para-Muitos (M:N) | Artigo ↔ Tag | Tabela de junção article_tag |
3. Teoria da Normalização
Primeira Forma Normal (1NF): Colunas devem ser atômicas
-- ❌ Viola 1NF: coluna phone armazena múltiplos valores
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
phones VARCHAR(200) -- '138xxx,139xxx,137xxx'
);
-- ✅ Conforme com 1NF: cada campo armazena um único valor atômico
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE user_phones (
id INT PRIMARY KEY,
user_id INT,
phone VARCHAR(20)
);
Segunda Forma Normal (2NF): Colunas não-chave devem depender totalmente de toda a chave primária
-- ❌ Viola 2NF: nos itens do pedido, product_name depende apenas de product_id, não de order_id
CREATE TABLE order_items (
order_id INT,
product_id INT,
product_name VARCHAR(100), -- Dependência parcial
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- ✅ Conforme com 2NF: dividir em uma tabela de produtos
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id)
);
Terceira Forma Normal (3NF): Colunas não-chave não devem ter dependências transitivas
-- ❌ Viola 3NF: city_name depende de city_id, city_id depende de id → dependência transitiva
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
city_id INT,
city_name VARCHAR(50) -- Dependência transitiva
);
-- ✅ Conforme com 3NF: dividir em uma tabela de cidades
CREATE TABLE cities (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
city_id INT
);
BCNF (Forma Normal de Boyce-Codd)
Mais rigorosa que 3NF: todo determinante deve ser uma chave candidata. Na prática, satisfazer 3NF geralmente é suficiente.
4. Desnormalização
Às vezes, para performance de consultas, a redundância é introduzida intencionalmente:
-- Armazenar contagem de comentários redundantemente na tabela de artigos para evitar COUNT toda vez
ALTER TABLE articles ADD COLUMN comment_count INT DEFAULT 0;
-- Atualizar o campo redundante
UPDATE articles SET comment_count = (
SELECT COUNT(*) FROM comments WHERE comments.article_id = articles.id
);
Quando desnormalizar:
- Leituras superam muito as escritas
- Consultas de agregação são extremamente frequentes
- Apenas otimização de índices não atende aos requisitos de performance
5. Design de Relacionamento entre Tabelas
Escolhas de Chave Primária:
| Tipo | Prós | Contras | Casos de Uso |
|---|---|---|---|
Auto-incremento AUTO_INCREMENT |
Ordenado, compacto, inserções rápidas | Previsível, problemas de fragmentação | Maioria das tabelas de negócio |
| UUID | Globalmente único, imprevisível | Tamanho grande, índices desordenados são lentos | Sistemas distribuídos |
| Chave primária composta | Semanticamente clara | Referências de chave estrangeira são complicadas | Tabelas de junção, tabelas de associação |
| ID Snowflake | Ordenado, globalmente único | Requer dependências extras | Alta concorrência distribuída |
-- Chave primária auto-incremento
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50)
);
-- Chave primária UUID
CREATE TABLE users (
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
name VARCHAR(50)
);
-- Chave primária composta (tabela de junção)
CREATE TABLE article_tag (
article_id BIGINT,
tag_id BIGINT,
PRIMARY KEY (article_id, tag_id)
);
6. Convenções de Nomenclatura de Campos
| Convenção | ✅ Recomendado | ❌ Evitar |
|---|---|---|
| Usar minúsculas + underscores | user_name |
UserName, username |
| Nomes de tabelas no plural | users |
user, tbl_user |
| Nomes de campos de chave estrangeira | user_id |
uid, userId |
| Campos booleanos | is_deleted |
deleted, flag |
| Campos de timestamp | created_at |
createTime, add_time |
| Campos monetários | DECIMAL(10,2) |
FLOAT, DOUBLE |
7. Melhores Práticas de Design
- Toda tabela deve ter uma chave primária
- Adicionar índices em colunas de chave estrangeira (mesmo que o banco de dados não imponha restrições de chave estrangeira)
- Escolher o menor tipo de dado suficiente:
TINYINTem vez deINTpara valores de status - Usar
DECIMALpara valores monetários, não tipos de ponto flutuante - Usar
DATETIMEouTIMESTAMPpara campos de tempo, não strings - Reservar campos de extensão:
extra JSONoustatuscom operações bitwise - Exclusão lógica em vez de exclusão física:
is_deleted TINYINT DEFAULT 0
💡 Sintaxe Básica
-- Modelo básico para criar uma tabela
CREATE TABLE table_name (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Chave primária',
-- Campos de negócio
name VARCHAR(100) NOT NULL COMMENT 'Nome',
status TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1-ativo 0-desabilitado',
-- Campo de chave estrangeira
user_id BIGINT NOT NULL COMMENT 'ID do usuário',
-- Campos de timestamp
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Data de criação',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Data de atualização',
-- Índices
INDEX idx_user_id (user_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Comentário da tabela';
Exemplo: Projetar um Sistema Simples de Seleção de Cursos de Estudantes (Dificuldade ⭐)
Requisitos: Gerenciar estudantes, cursos e registros de matrícula.
-- Tabela de estudantes
CREATE TABLE students (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID do estudante',
student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 'Número do estudante',
name VARCHAR(50) NOT NULL COMMENT 'Nome',
gender TINYINT NOT NULL DEFAULT 0 COMMENT 'Gênero: 0-desconhecido 1-masculino 2-feminino',
enrollment_year INT NOT NULL COMMENT 'Ano de matrícula',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de estudantes';
-- Tabela de cursos
CREATE TABLE courses (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID do curso',
course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 'Número do curso',
name VARCHAR(100) NOT NULL COMMENT 'Nome do curso',
credit DECIMAL(3,1) NOT NULL COMMENT 'Créditos',
teacher VARCHAR(50) COMMENT 'Professor',
max_students INT NOT NULL DEFAULT 60 COMMENT 'Máximo de matrículas',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de cursos';
-- Tabela de matrículas (tabela de junção, relacionamento muitos-para-muitos)
CREATE TABLE enrollments (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
student_id BIGINT NOT NULL COMMENT 'ID do estudante',
course_id BIGINT NOT NULL COMMENT 'ID do curso',
score DECIMAL(5,2) COMMENT 'Nota',
enrolled_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Data da matrícula',
UNIQUE KEY uk_student_course (student_id, course_id),
INDEX idx_course_id (course_id),
INDEX idx_student_id (student_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de matrículas';
Exemplo: Projetar Tabelas Principais para um Sistema de Pedidos de E-commerce (Dificuldade ⭐⭐)
-- Tabela de categorias de produtos
CREATE TABLE categories (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL COMMENT 'Nome da categoria',
parent_id BIGINT DEFAULT NULL COMMENT 'ID da categoria pai, NULL para nível superior',
sort_order INT NOT NULL DEFAULT 0 COMMENT 'Ordem de classificação',
INDEX idx_parent_id (parent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de categorias de produtos';
-- Tabela de produtos
CREATE TABLE products (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
category_id BIGINT NOT NULL COMMENT 'ID da categoria',
name VARCHAR(200) NOT NULL COMMENT 'Nome do produto',
price DECIMAL(10,2) NOT NULL COMMENT 'Preço de venda',
stock INT NOT NULL DEFAULT 0 COMMENT 'Estoque',
status TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1-listado 0-não listado',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_category (category_id),
INDEX idx_status_price (status, price)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de produtos';
-- Tabela de pedidos (desnormalização: armazenar snapshot do endereço de entrega)
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 'Número do pedido',
user_id BIGINT NOT NULL COMMENT 'ID do usuário',
total_amount DECIMAL(12,2) NOT NULL COMMENT 'Valor total do pedido',
status TINYINT NOT NULL DEFAULT 0 COMMENT 'Status: 0-pagamento pendente 1-pago 2-enviado 3-concluído 4-cancelado',
receiver_name VARCHAR(50) NOT NULL COMMENT 'Nome do destinatário',
receiver_phone VARCHAR(20) NOT NULL COMMENT 'Telefone do destinatário',
receiver_address VARCHAR(500) NOT NULL COMMENT 'Endereço de entrega',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
paid_at DATETIME COMMENT 'Data do pagamento',
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de pedidos';
-- Tabela de itens do pedido
CREATE TABLE order_items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL COMMENT 'ID do pedido',
product_id BIGINT NOT NULL COMMENT 'ID do produto',
product_name VARCHAR(200) NOT NULL COMMENT 'Nome do produto (snapshot)',
product_price DECIMAL(10,2) NOT NULL COMMENT 'Preço unitário (snapshot)',
quantity INT NOT NULL COMMENT 'Quantidade',
subtotal DECIMAL(12,2) NOT NULL COMMENT 'Subtotal',
INDEX idx_order_id (order_id),
INDEX idx_product_id (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de itens do pedido';
Destaques do Design:
- A tabela de pedidos armazena redundantemente o endereço de entrega (snapshot no momento do pedido), para que pedidos históricos não sejam afetados mesmo que o usuário altere seu endereço depois
- A tabela de itens do pedido armazena redundantemente o nome e preço do produto, para que pedidos existentes não sejam afetados mesmo que os preços dos produtos mudem
- Campos monetários usam uniformemente
DECIMAL(12,2) - Campos de status usam
TINYINTcom comentários explicando cada valor
🏢 Cenário 1: Projetar o Banco de Dados de um Sistema de Blog
Requisitos: Suportar registro de usuários, escrita de artigos, tags e postagem de comentários.
-- Tabela de usuários
CREATE TABLE blog_users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
avatar_url VARCHAR(500),
bio TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de usuários do blog';
-- Tabela de artigos
CREATE TABLE blog_articles (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
category_id BIGINT,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-rascunho 1-publicado 2-não listado',
view_count INT NOT NULL DEFAULT 0,
like_count INT NOT NULL DEFAULT 0,
published_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_category (category_id),
INDEX idx_status_published (status, published_at DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de artigos';
-- Tabela de tags
CREATE TABLE blog_tags (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de tags';
-- Tabela de junção artigo-tag
CREATE TABLE blog_article_tag (
article_id BIGINT NOT NULL,
tag_id BIGINT NOT NULL,
PRIMARY KEY (article_id, tag_id),
INDEX idx_tag_id (tag_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de associação artigo-tag';
-- Tabela de comentários (suporta comentários aninhados)
CREATE TABLE blog_comments (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
article_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
parent_id BIGINT COMMENT 'ID do comentário pai, NULL para comentários de nível superior',
content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_article_id (article_id),
INDEX idx_parent_id (parent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de comentários';
🏢 Cenário 2: Design de Sistema SaaS Multi-Tenant
Requisitos: Um único sistema servindo múltiplos clientes empresariais com isolamento de dados.
-- Tabela de tenants
CREATE TABLE tenants (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL COMMENT 'Nome da empresa',
plan VARCHAR(20) NOT NULL DEFAULT 'free' COMMENT 'Plano: free/basic/pro',
max_users INT NOT NULL DEFAULT 10,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de tenants';
-- Tabela de usuários (cada registro pertence a um tenant)
CREATE TABLE tenant_users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT NOT NULL COMMENT 'ID do tenant',
username VARCHAR(50) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'member',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_tenant_username (tenant_id, username),
INDEX idx_tenant_id (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de usuários do tenant';
-- Todas as tabelas de negócio incluem o campo tenant_id
CREATE TABLE tenant_orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tenant_id (tenant_id),
INDEX idx_tenant_user (tenant_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Tabela de pedidos do tenant';
WHERE tenant_id = ? para prevenir vazamento de dados.
❓ Perguntas Frequentes
P: Quando devo dividir tabelas e quando devo manter uma tabela grande? R: Dividir quando certos campos são atualizados com muito mais frequência que outros (ex.: informações básicas do usuário vs. logs de login); manter uma tabela quando os campos são consultados juntos com frequência.
P: Como escolher entre exclusão lógica e exclusão física? R: Usar exclusão lógica (
is_deleted) para dados principais como finanças e pedidos; usar exclusão física para dados auxiliares como logs e caches.
P: Em quais cenários campos JSON são adequados? R: Para armazenar atributos de extensão com estrutura inconsistente e baixa frequência de consulta, como preferências do usuário ou especificações de produtos. Não coloque campos que precisam de consultas indexadas em JSON.
P: Qual é melhor, IDs auto-incremento ou UUIDs? R: Prefira IDs auto-incremento para sistemas de servidor único (ordenados, compactos); considere UUIDs ou algoritmos snowflake para sistemas distribuídos (globalmente únicos).
📖 Resumo
Esta lição cobriu sistematicamente a metodologia completa de design de banco de dados:
- Análise de Requisitos é o ponto de partida do design, esclarecendo entidades, relacionamentos e padrões de consulta
- Diagramas ER ajudam a visualizar relacionamentos um-para-um, um-para-muitos e muitos-para-muitos entre entidades
- Normalização (1NF→2NF→3NF→BCNF) elimina progressivamente a redundância de dados
- Desnormalização introduz moderadamente redundância em gargalos de performance
- Seleção de Chave Primária requer avaliar prós e contras de auto-incremento, UUID e chaves compostas
- Convenções de Nomenclatura garantem consistência na colaboração da equipe
📝 Exercícios
- Projete um banco de dados para um sistema de exame online, incluindo: usuários, provas, questões, alternativas, registros de respostas e notas. Desenhe um diagrama ER e escreva as instruções CREATE TABLE.
- Normalize a seguinte tabela:
orders(order_id, customer_name, customer_phone, product_name, product_price, quantity), dividindo-a até pelo menos 3NF. - Escolha uma estratégia de chave primária adequada (auto-incremento vs. UUID) para seu projeto e explique o raciocínio.
Próxima Lição →26-sql-injection.md



