TutorialMay 30, 202610 min de leitura

Tutorial de Design de Banco de Dados MySQL: Do Schema à Produção

MySQL é o banco de dados open-source mais implantado do mundo. Este tutorial cobre decisões de design específicas do MySQL — motores de armazenamento, tipos de dados, conjuntos de caracteres, indexação, chaves estrangeiras e estratégias de migration — para levar seu schema do primeiro rascunho à produção.

MySQL alimenta mais aplicações web do que qualquer outro banco de dados relacional. É o "M" na stack LAMP, o padrão em inúmeros ambientes de hospedagem compartilhada e ainda é o banco de dados de escolha para muitos sistemas de produção de alto tráfego. Mas MySQL tem peculiaridades e decisões de design importantes que diferem do PostgreSQL ou SQLite — e conhecê-las é essencial para construir um schema com bom desempenho e que evita bugs sutis.

Este tutorial percorre o design de schema específico do MySQL desde o design inicial até o deploy em produção, cobrindo cada ponto de decisão que importa.

Motores de Armazenamento: Sempre Use InnoDB

MySQL suporta múltiplos motores de armazenamento, mas o único que você deve usar para tabelas de aplicação em 2026 é InnoDB. InnoDB suporta chaves estrangeiras, transações, bloqueio em nível de linha e recuperação de falhas. O motor legado MyISAM não tem suporte a chaves estrangeiras e usa bloqueio em nível de tabela, tornando-o inadequado para qualquer aplicação com escritas concorrentes.

InnoDB é o padrão desde o MySQL 5.5, então você não deveria precisar especificá-lo explicitamente. No entanto, se você está trabalhando com um banco de dados mais antigo ou migrando de outro sistema, verifique com SHOW TABLE STATUS que todas as tabelas usam InnoDB. Se qualquer tabela usar MyISAM, converta-a: ALTER TABLE table_name ENGINE = InnoDB;

Tipos de Inteiro: INT vs BIGINT

Os tipos de inteiro do MySQL diferem do PostgreSQL em nomenclatura e intervalo. Use `INT` (4 bytes, intervalo com sinal -2,1B a 2,1B, sem sinal 0 a 4,3B) para a maioria dos IDs e contagens. Use `BIGINT` (8 bytes, intervalo com sinal ±9,2 quintilhões) para tabelas que crescerão além de 2 bilhões de linhas. O modificador `UNSIGNED` dobra o intervalo positivo sem custo de armazenamento — INT UNSIGNED vai até 4.294.967.295 — e é uma boa escolha para colunas de ID que nunca serão negativas.

Para chaves primárias auto-incrementadas, a declaração é: id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY. Prefira BIGINT UNSIGNED AUTO_INCREMENT para qualquer tabela que possa escalar significativamente. O tipo TINYINT(1) é o booleano convencional do MySQL (0 ou 1); ORMs como Laravel e Doctrine mapeiam booleanos PHP para esse tipo automaticamente.

VARCHAR vs TEXT: Uma Distinção Crítica

Ao contrário do PostgreSQL, MySQL armazena `VARCHAR` e `TEXT` de forma diferente, e a distinção importa para a performance. VARCHAR(n) é armazenado inline com a linha para valores de até 65.535 bytes (dentro do limite do formato de linha). TEXT (e MEDIUMTEXT, LONGTEXT) armazena os dados fora da página e mantém apenas um ponteiro inline — isso o torna mais lento para ler valores pequenos.

A orientação prática: use VARCHAR(n) para strings onde você conhece um limite máximo razoável — nomes, e-mails, slugs, códigos de status. Use TEXT para conteúdo verdadeiramente aberto — corpos de posts de blog, documentos markdown, mensagens de log. Uma ressalva importante: MySQL não pode usar uma coluna TEXT como parte de um índice UNIQUE ou de um índice regular sem um comprimento de prefixo (INDEX (column(191))). Se você precisar indexar a coluna, use VARCHAR(191) (o máximo para utf8mb4 com o comprimento de índice padrão).

Sempre Use utf8mb4, Não utf8

Esta é uma das pegadinhas mais notórias do MySQL. O conjunto de caracteres utf8 do MySQL não é UTF-8 real — ele suporta apenas caracteres que cabem em 3 bytes, o que exclui emoji e muitos scripts não-latinos. Sempre use `utf8mb4`, que é a implementação genuína de 4 bytes do UTF-8.

Defina o conjunto de caracteres nos níveis de servidor, banco de dados, tabela e coluna. A abordagem mais segura é configurar os padrões do MySQL: ``sql CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; E especificá-lo em colunas VARCHAR e TEXT: sql name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL Para comparações sem distinção de maiúsculas/minúsculas (o caso comum), use utf8mb4_unicode_ci. Para comparações com distinção, use utf8mb4_bin`.

DATETIME vs TIMESTAMP

MySQL tem dois tipos para armazenar valores de data e hora. `TIMESTAMP` armazena valores como timestamp Unix UTC internamente e converte de/para o fuso horário local do servidor na leitura/escrita. Suporta um intervalo de 1970 a 2038 — o famoso "problema do ano 2038." `DATETIME` armazena valores como estão, sem conversão de fuso horário, suportando um intervalo de 1000 a 9999.

Para colunas created_at e updated_at, use DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP e DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP. Isso evita o limite de 2038 e a confusão de conversão implícita de fuso horário. Para colunas que armazenam datas e horas voltadas ao usuário, armazene-as em UTC como DATETIME e converta para o fuso horário do usuário no código da aplicação.

Chaves Estrangeiras e InnoDB: A Sintaxe Importa

A sintaxe de chave estrangeira do MySQL é ligeiramente diferente do PostgreSQL, e é rígida em certas regras. Colunas de chave estrangeira devem ter o mesmo tipo e sinal que a chave primária referenciada: se users.id é INT UNSIGNED, então orders.user_id também deve ser INT UNSIGNED. A coluna de chave estrangeira deve ser indexada (MySQL nem sempre cria esse índice automaticamente em todas as versões). ``sql CREATE TABLE orders ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'pending', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX idx_orders_user_id (user_id), CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE );

Pegadinhas Comuns do MySQL para Evitar

Vários comportamentos do MySQL surpreendem desenvolvedores. Truncamento silencioso: versões mais antigas do MySQL truncam strings que excedem o comprimento de um VARCHAR sem gerar erro no modo não-estrito. Habilite o modo estrito: SET sql_mode = 'STRICT_TRANS_TABLES,...' ou configure no my.cnf. Datas zero: MySQL permite 0000-00-00 como valor de data por padrão; desabilite isso com NO_ZERO_DATE no sql_mode. Comportamento de GROUP BY: MySQL historicamente permitia selecionar colunas não agregadas que não estão no GROUP BY; o modo ONLY_FULL_GROUP_BY impõe o comportamento SQL padrão e deve sempre estar habilitado.

Estratégias de Migration para MySQL

Para evoluir schemas MySQL em produção, duas abordagens dominam. DDL Online (suportado desde MySQL 5.6+): muitas operações ALTER TABLE no InnoDB podem ser realizadas com ALGORITHM=INPLACE, LOCK=NONE, permitindo que a tabela permaneça legível e gravável durante a migration. Verifique a documentação do MySQL para quais operações suportam DDL online. Para tabelas grandes onde DDL online não é suportado, use pt-online-schema-change (Percona Toolkit) ou gh-ost (ferramenta de schema online do GitHub) para realizar a migration sem bloquear a tabela.

Projetando Schemas MySQL com ER Flow

ER Flow suporta tipos de dados MySQL nativamente, incluindo INT, BIGINT UNSIGNED, VARCHAR, TEXT, DATETIME, TIMESTAMP e TINYINT(1) para booleanos. Você pode projetar seu schema completo visualmente — tabelas, colunas, chaves estrangeiras, índices — e gerar SQL de migration compatível com MySQL diretamente do diagrama ER. As migrations geradas usam sintaxe MySQL adequada incluindo ENGINE=InnoDB, declarações corretas de conjunto de caracteres e criação explícita de índices para colunas de chave estrangeira. Comece com o modelo visual, gere a migration e faça o deploy com confiança.