Design de Banco de Dados

Design de Banco de Dados

🎯 Analogia da Vida Real

Imagine que você está abrindo uma biblioteca:

📚 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:

TEXT
[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

SQL
-- ❌ 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

SQL
-- ❌ 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

SQL
-- ❌ 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:

SQL
-- 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:

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
SQL
-- 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

  1. Toda tabela deve ter uma chave primária
  2. Adicionar índices em colunas de chave estrangeira (mesmo que o banco de dados não imponha restrições de chave estrangeira)
  3. Escolher o menor tipo de dado suficiente: TINYINT em vez de INT para valores de status
  4. Usar DECIMAL para valores monetários, não tipos de ponto flutuante
  5. Usar DATETIME ou TIMESTAMP para campos de tempo, não strings
  6. Reservar campos de extensão: extra JSON ou status com operações bitwise
  7. Exclusão lógica em vez de exclusão física: is_deleted TINYINT DEFAULT 0

💡 Sintaxe Básica

SQL
-- 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';
💡 Dica: Gastar 1 hora extra no design pode economizar 10 horas de refatoração depois. Desenhe um diagrama ER no papel primeiro, depois escreva SQL.

Exemplo: Projetar um Sistema Simples de Seleção de Cursos de Estudantes (Dificuldade ⭐)

Requisitos: Gerenciar estudantes, cursos e registros de matrícula.

SQL
-- 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';
▶ Experimente

Exemplo: Projetar Tabelas Principais para um Sistema de Pedidos de E-commerce (Dificuldade ⭐⭐)

SQL
-- 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';
▶ Experimente

Destaques do Design:

🏢 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.

SQL
-- 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.

SQL
-- 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';
💡 Chave: Toda consulta SQL deve incluir uma condição 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:

📝 Exercícios

  1. 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.
  2. Normalize a seguinte tabela: orders(order_id, customer_name, customer_phone, product_name, product_price, quantity), dividindo-a até pelo menos 3NF.
  3. 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

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%