O que é MySQL?
MySQL é um banco de dados relacional de código aberto que se tornou a camada de armazenamento padrão da web primitiva. Foi lançado em 1995 com foco em velocidade e simplicidade para sites com alta carga de leitura, e cresceu junto com a stack LAMP — Linux, Apache, MySQL e PHP — tornando-se um dos bancos de dados mais amplamente implantados que existem.
Atualmente, ele é desenvolvido pela Oracle, mas uma grande comunidade e um rico conjunto de serviços gerenciados o mantêm presente em todos os lugares. WordPress, Magento, plataformas de e-commerce no estilo Shopify e inúmeras aplicações customizadas rodam em MySQL. Se você já utilizou um site com formulário de login, há uma boa chance de que uma tabela MySQL estivesse envolvida.
O MySQL moderno não é mais aquele motor simples dos anos 90. Desde a versão 8, ele possui um dicionário de dados transacional, common table expressions, window functions e um suporte robusto a JSON. Aprendê-lo hoje significa aprender um banco de dados relacional sério que, por acaso, também é extraordinariamente bem suportado.
Onde o MySQL é executado
A maior vantagem prática do MySQL é a sua ubiquidade. Quase todo host compartilhado, provedor de nuvem e plataforma-as-a-service o oferece. Frameworks já trazem drivers integrados, ORMs oferecem suporte nativo e DBAs possuem décadas de experiência com ele. Isso reduz o custo de tudo ao redor do banco de dados: contratação, ferramentas, monitoramento e migração.
É a escolha habitual para sistemas de gerenciamento de conteúdo, e-commerce, backends de SaaS e qualquer aplicação onde o ecossistema importa tanto quanto o motor. Ele escala de uma única instância pequena para clusters com sharding, e o caminho de um para o outro é amplamente documentado.
InnoDB é o engine que importa
O MySQL possui uma arquitetura de storage engine plugável, mas, na prática, você usará o InnoDB. Ele oferece:
- Transações ACID com
COMMITeROLLBACK. - Row-level locking (bloqueio a nível de linha), para que escritores não bloqueiem leitores.
- Foreign keys e aplicação de constraints.
- Crash recovery através de um redo log.
- MVCC para leituras consistentes.
O engine MyISAM, mais antigo, não possuía transações nem row-level locking. Ele não tem espaço em um novo schema. Sempre declare ENGINE=InnoDB explicitamente para que a escolha fique visível e não dependa dos padrões do servidor.
CREATE TABLE products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
sku VARCHAR(64) NOT NULL,
name VARCHAR(200) NOT NULL,
price_cents INT UNSIGNED NOT NULL,
stock INT NOT NULL DEFAULT 0,
attributes JSON NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_products_sku (sku)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Tipos de dados e AUTO_INCREMENT
O sistema de tipos do MySQL é pragmático. As escolhas mais comuns:
INTeBIGINT— números inteiros; adicioneUNSIGNEDpara IDs e contagens que não podem ser negativas.VARCHAR(n)— texto de comprimento variável com um limite máximo. Ao contrário do Postgres, o MySQL realmente se beneficia de um comprimento sensato.TEXT— textos grandes, armazenados fora da linha quando necessário.DECIMAL(p, s)— decimais exatos, o tipo correto para dinheiro caso você não utilize unidades menores em inteiros.TIMESTAMPeDATETIME— timestamps;TIMESTAMPconverte para UTC e possui um limite de intervalo, enquantoDATETIMEarmazena exatamente o que você fornece.JSON— um documento JSON validado e armazenado de forma eficiente.ENUM— um conjunto fixo de strings; conveniente, mas alterar a lista exige uma mudança no schema.
O AUTO_INCREMENT gera o próximo número inteiro para uma coluna, quase sempre a chave primária. Ele é rápido e tolerante a lacunas: inserts que sofreram rollback consomem um valor, e inserts concorrentes podem não produzir números contíguos. Nunca dependa de que o ID não tenha lacunas ou de que sua ordem signifique algo.
INSERT INTO products (sku, name, price_cents, stock, attributes)
VALUES ('SKU-1', 'Widget', 999, 10, JSON_OBJECT('colour', 'blue'));
SELECT LAST_INSERT_ID();
CRUD sem surpresas
As quatro operações básicas mapeiam-se para quatro instruções. Leia-as como um conjunto, pois a estrutura se repete.
INSERT INTO products (sku, name, price_cents, stock, attributes)
VALUES ('SKU-2', 'Gadget', 1499, 5, JSON_OBJECT('colour', 'red'));
SELECT id, sku, name, price_cents
FROM products
WHERE stock > 0
ORDER BY price_cents ASC
LIMIT 20 OFFSET 40;
UPDATE products
SET price_cents = 1299, stock = stock - 1
WHERE id = 42;
DELETE FROM products
WHERE stock = 0 AND created_at < NOW() - INTERVAL 90 DAY;
Dois hábitos evitam os acidentes clássicos. Primeiro, sempre inclua uma cláusula WHERE em UPDATE e DELETE; sem ela, todas as linhas serão alteradas. Segundo, execute o SELECT equivalente primeiro para confirmar quais linhas você está prestes a afetar. O MySQL possui um modo sql_safe_updates que recusa instruções sem uma chave no WHERE, e ativá-lo em desenvolvimento é uma salvaguarda simples e eficaz.
LIMIT com OFFSET faz a paginação, mas offsets grandes escaneiam e descartam linhas. Para paginações profundas, prefira a paginação por keyset: WHERE id > :last_id ORDER BY id LIMIT 20.
Joins e agregação
Joins combinam tabelas com base em uma condição de correspondência, exatamente como no SQL padrão.
SELECT c.name AS category,
COUNT(*) AS product_count,
SUM(p.price_cents) AS inventory_value
FROM products p
JOIN categories c ON c.id = p.category_id
WHERE p.stock > 0
GROUP BY c.id, c.name
ORDER BY inventory_value DESC
LIMIT 20;
JOIN mantém os pares correspondentes, LEFT JOIN mantém todas as linhas da esquerda e preenche as colunas da direita ausentes com NULL. Agregadores como COUNT, SUM, AVG, MIN e MAX colapsam grupos; toda coluna selecionada que não seja agregada deve ser agrupada.
Historicamente, o MySQL permitia a seleção de colunas não agrupadas e retornava um valor arbitrário, o que escondia bugs. Com o ONLY_FULL_GROUP_BY ativado — o padrão no MySQL 8 — o servidor rejeita queries ambíguas, que é o comportamento desejado. Não o desative para fazer uma query antiga funcionar; corrija a query.
WHERE filtra linhas antes do agrupamento e HAVING filtra depois, portanto, condições sobre agregadores pertencem ao HAVING.
Índices e EXPLAIN
Um índice é uma estrutura ordenada que evita a varredura completa da tabela (full table scan). O MySQL cria automaticamente um para a chave primária e para cada constraint UNIQUE. Adicione outros para as colunas que você utiliza em filtros, joins e ordenações.
CREATE INDEX idx_products_price ON products (price_cents);
Um índice composto abrange várias colunas e segue a regra do prefixo à esquerda (leftmost-prefix rule): um índice em (category_id, price_cents) ajuda consultas que filtram por category_id, ou por category_id e price_cents, mas não por price_cents sozinho. Ordene as colunas da mais seletiva e filtrada com mais frequência para as menos.
Pergunte ao otimizador o que ele fará:
EXPLAIN
SELECT id, name, price_cents
FROM products
WHERE price_cents < 2500
ORDER BY price_cents
LIMIT 25;
Leia a coluna type primeiro: const, eq_ref e ref são bons; range é aceitável; index e ALL indicam uma varredura. A coluna key mostra qual índice foi escolhido, e rows estima quantas linhas serão examinadas. Um rows alto para um resultado pequeno geralmente significa um índice ausente ou inutilizável. Use EXPLAIN ANALYZE no MySQL 8 para executar a query e ver os tempos reais.
Os índices de cobertura (covering indexes) merecem menção. Se um índice contém todas as colunas que uma query precisa, o MySQL pode responder apenas com o índice e nunca tocar na linha da tabela. Adicionar uma coluna a um índice puramente para torná-lo de cobertura costuma trazer um ganho significativo de performance.
utf8mb4 e a armadilha do charset
Os conjuntos de caracteres são onde o MySQL costuma surpreender as pessoas. Durante a maior parte de sua história, o utf8 padrão suportava no máximo três bytes por caractere, o que cobre a maioria dos textos, mas não os code points de quatro bytes, como emojis e diversos scripts raros. Tentar armazenar um emoji em uma coluna utf8 gera um erro ou trunca o valor, dependendo do modo do servidor.
A solução é o utf8mb4, que é o UTF-8 real e armazena tudo. Configure-o em todos os níveis — servidor, banco de dados, tabela e conexão:
CREATE DATABASE shop
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
A collation decide como as strings são comparadas e ordenadas. A utf8mb4_0900_ai_ci é insensível a acentos e maiúsculas/minúsculas (case-insensitive), que é geralmente o que os usuários esperam em buscas. As collations também afetam o comportamento dos índices, portanto, mantenha-as consistentes em colunas relacionadas (joins); uma divergência força conversões que podem tornar um índice inutilizável.
Transações e níveis de isolamento
O InnoDB oferece transações. Agrupe escritas relacionadas para que elas tenham sucesso ou falhem juntas.
START TRANSACTION;
UPDATE inventory SET quantity = quantity - 1
WHERE product_id = 42 AND quantity >= 1;
INSERT INTO orders (product_id, quantity)
VALUES (42, 1);
COMMIT;
Verifique as linhas afetadas e ROLLBACK se a atualização protegida não alterou nada. O nível de isolamento padrão é o Repeatable Read, que fornece um snapshot consistente para a transação e, no InnoDB, utiliza gap locks que evitam phantom rows. Os outros níveis são Read Uncommitted, Read Committed e Serializable.
Uma diferença útil em relação ao Postgres: devido ao gap locking no Repeatable Read, inserts concorrentes em um intervalo podem causar bloqueios ou deadlocks com mais facilidade. Mantenha as transações curtas, atualize as linhas em uma ordem consistente e esteja preparado para tentar novamente em caso de deadlock, que o InnoDB reporta como um erro em vez de corromper o estado.
Replicação e escalonamento de leitura
A maioria das implantações de MySQL escala as leituras antes das escritas. O primário registra cada alteração em seu binary log, e uma ou mais réplicas se conectam e reproduzem esse log. A replicação é assíncrona por padrão, portanto, uma réplica pode apresentar um atraso (lag) em relação ao primário de milissegundos ou mais.
O padrão comum é enviar as escritas para o primário e distribuir as leituras entre as réplicas, aceitando que uma leitura possa retornar brevemente dados levemente desatualizados. Para consistência de leitura após a escrita (read-after-write consistency), direcione as leituras de um usuário para o primário por um curto período após a escrita dele, ou utilize a réplica apenas para dados que toleram esse atraso.
A replicação também proporciona alta disponibilidade. Se o primário falhar, uma réplica pode ser promovida. Ferramentas como orquestradores e serviços gerenciados automatizam esse failover, mas você ainda precisa entender a compensação (trade-off) entre os modos síncrono e assíncrono: o síncrono aguarda as réplicas e coloca a disponibilidade em risco, enquanto o assíncrono corre o risco de perder as últimas transações em caso de failover.
Upserts com ON DUPLICATE KEY UPDATE
O upsert idiomático do MySQL é o INSERT ... ON DUPLICATE KEY UPDATE. Quando o insert violaria uma chave primária ou única, a cláusula de update é executada em seu lugar.
INSERT INTO products (sku, name, price_cents, stock, attributes)
VALUES ('SKU-1', 'Widget', 1099, 5, JSON_OBJECT('colour', 'blue'))
ON DUPLICATE KEY UPDATE
price_cents = VALUES(price_cents),
stock = stock + VALUES(stock);
Isso é atômico, o que é fundamental para contadores e inventários. A alternativa — fazer um select e então decidir se deve inserir ou atualizar — possui uma janela de race condition onde duas sessões podem não encontrar a linha e ambas realizarem o insert. Use o upsert sempre que a operação for genuinamente de inserção ou atualização.
Colunas JSON
O tipo JSON do MySQL armazena um documento validado e oferece suporte a funções para ler e escrever partes dele.
SELECT id, name,
attributes->>'$.colour' AS colour
FROM products
WHERE attributes->>'$.colour' = 'blue';
Você pode indexar JSON utilizando colunas geradas. Como o MySQL não consegue indexar uma expressão JSON diretamente, extraia-a para uma coluna gerada armazenada (stored generated column) e indexe essa coluna:
ALTER TABLE products
ADD COLUMN colour VARCHAR(32)
GENERATED ALWAYS AS (attributes->>'$.colour') STORED,
ADD INDEX idx_products_colour (colour);
Use JSON para atributos variáveis e payloads de terceiros. Mantenha os campos que você filtra constantemente como colunas reais; o truque da coluna gerada funciona, mas uma coluna nativa com um tipo real é mais simples e rápida.
Stored procedures: use com moderação
O MySQL suporta stored procedures, funções, triggers e eventos agendados. Eles podem reduzir as idas e vindas (round trips) ao servidor e centralizar a lógica, mas também movem a lógica de negócio para uma linguagem que é mais difícil de testar, versionar e depurar do que o código da sua aplicação.
Use-os para tarefas que sejam genuinamente voltadas ao banco de dados: manutenção em massa, migrações de dados e jobs que precisem rodar próximos aos dados. Evite colocar regras centrais de domínio em procedures que apenas uma equipe compreende. Triggers são especialmente fáceis de esquecer; um UPDATE que dispara silenciosamente três triggers é difícil de analisar, e efeitos colaterais ocultos acabam surpreendendo a todos eventualmente.
Usuários, permissões e backups
Crie um usuário de aplicação dedicado apenas com os privilégios necessários, em vez de se conectar como root.
CREATE USER 'app'@'%' IDENTIFIED BY 'a-strong-password';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'app'@'%';
FLUSH PRIVILEGES;
Mantenha o DDL e as migrations em uma conta separada e com mais privilégios. Restrinja os hosts sempre que possível, exija TLS e rotacione as credenciais.
Para backups, o mysqldump gera um dump lógico que é fácil de mover e restaurar:
mysqldump --single-transaction --routines --triggers shop > shop.sql
mysql shop_restore < shop.sql
A flag --single-transaction tira um snapshot consistente das tabelas InnoDB sem travá-las durante todo o dump. Para bancos de dados grandes onde o tempo de dump é proibitivo, utilize uma ferramenta física como o Percona XtraBackup. De qualquer forma, registre a posição do binary log para que você possa realizar a recuperação para um ponto específico no tempo (point-in-time recovery) e teste as restaurações.
MySQL vs MariaDB
O MariaDB começou como um fork comunitário do MySQL após a aquisição pela Oracle e divergiu desde então. Ele mantém a maior parte da sintaxe do MySQL e adiciona seus próprios storage engines e funcionalidades. Enquanto isso, o MySQL evoluiu rapidamente desde a versão 8, trazendo um novo dicionário de dados, window functions e melhorias no JSON.
Para a maioria das aplicações, as diferenças são pequenas. Escolha o MySQL se o seu provedor de banco de dados gerenciado ou contrato de suporte for baseado nele, e o MariaDB se você preferir um projeto governado pela comunidade ou precisar de um de seus engines. Os conceitos relacionais, SQL e padrões operacionais são transferíveis entre eles, portanto, a escolha raramente é irreversível.
Busca textual (Full-text search) no InnoDB
O InnoDB já vem com um índice de texto completo, portanto, um recurso de busca não exige um serviço separado. Crie um índice FULLTEXT nas colunas de texto e faça a consulta com MATCH ... AGAINST.
CREATE FULLTEXT INDEX ft_products_search
ON products (name, attributes);
SELECT id, name,
MATCH(name, attributes) AGAINST ('running shoe' IN NATURAL LANGUAGE MODE) AS score
FROM products
WHERE MATCH(name, attributes) AGAINST ('running shoe' IN NATURAL LANGUAGE MODE)
ORDER BY score DESC
LIMIT 20;
O NATURAL LANGUAGE MODE classifica por relevância e ignora palavras que aparecem na maioria das linhas. O BOOLEAN MODE oferece operadores como +must, -exclude e "exact phrase" para maior controle. O índice possui um comprimento mínimo de token controlado por innodb_ft_min_token_size, portanto, palavras muito curtas podem ser ignoradas.
A busca textual é ideal para catálogos de produtos e busca de artigos. Migre para um motor dedicado quando precisar de tolerância a erros de digitação (typo tolerance), faceting ou análise multilíngue que o MySQL não fornece.
Views e colunas geradas
Uma view é uma consulta nomeada que se comporta como uma tabela, sendo útil para empacotar um formato comum de dados e para limitar quais colunas um usuário de relatórios pode visualizar.
CREATE VIEW in_stock AS
SELECT id, sku, name, price_cents, stock
FROM products
WHERE stock > 0;
SELECT * FROM in_stock WHERE price_cents < 2500;
O MySQL 8 suporta window functions, portanto, as views também podem pré-formatar resultados analíticos. Lembre-se de que uma view executa sua consulta subjacente toda vez que é chamada, a menos que seja materializada manualmente em uma tabela; o MySQL não possui materialized views nativas, então as equipes utilizam ou uma tabela de resumo atualizada via agendamento ou um evento.
Colunas geradas, demonstradas na seção de JSON, são a outra metade desse conceito: uma coluna gerada do tipo stored é computada na escrita e pode ser indexada, enquanto uma virtual é computada na leitura. Use uma coluna stored quando precisar indexar a expressão, e uma virtual quando precisar do valor apenas ocasionalmente.
Alterando schemas em um banco de dados em produção
ALTER TABLE em uma tabela InnoDB grande pode reconstruir a tabela e manter locks por um longo período. O MySQL 8 suporta DDL online para diversas operações através do ALGORITHM=INPLACE, que reconstrói a tabela sem bloquear leituras e escritas simultâneas em muitos casos.
ALTER TABLE products
ADD COLUMN updated_at TIMESTAMP NULL,
ALGORITHM=INPLACE, LOCK=NONE;
ALTER TABLE products
ADD INDEX idx_products_updated (updated_at),
ALGORITHM=INPLACE, LOCK=NONE;
Nem toda alteração é online. Alterar o tipo de uma coluna, adicionar um índice FULLTEXT ou reconstruir uma chave primária geralmente ainda exige a cópia da tabela. Para esses casos, utilize ferramentas como pt-online-schema-change ou gh-ost, que criam uma tabela espelho (shadow table), copiam as linhas em lotes e fazem a substituição com o mínimo de bloqueio. Independentemente do método, execute as alterações de schema através de migrações versionadas e teste-as primeiro em uma cópia dos dados de produção.
Encontrando queries lentas
O MySQL registra queries que excedem long_query_time no slow query log, e o EXPLAIN mostra como uma query específica é executada. Juntos, eles são o caminho mais rápido para ir de “o app está lento” até uma correção concreta.
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.2;
SHOW VARIABLES LIKE 'slow_query_log_file';
O performance_schema e o schema sys resumem os mesmos dados. Comece pelas instruções que consomem a maior quantidade de tempo total, e não apenas a query individual mais lenta, e verifique o EXPLAIN para identificar full scans em tabelas grandes. O SHOW PROFILE e o EXPLAIN ANALYZE no MySQL 8 adicionam tempos por etapa para quando você precisar de uma análise mais profunda.
Carregando dados rapidamente
Um loop INSERT linha por linha é a maneira mais lenta de carregar dados. Agrupe várias linhas em um único comando, o que reduz as viagens de ida e volta (round trips) e permite que o InnoDB escreva as páginas de forma eficiente.
INSERT INTO products (sku, name, price_cents, stock, attributes)
VALUES
('SKU-10', 'Widget', 999, 5, JSON_OBJECT('colour', 'blue')),
('SKU-11', 'Gadget', 1499, 3, JSON_OBJECT('colour', 'red')),
('SKU-12', 'Gizmo', 2499, 7, JSON_OBJECT('colour', 'green'));
Para importações massivas, o LOAD DATA INFILE transmite um arquivo diretamente para uma tabela e é drasticamente mais rápido do que qualquer forma de INSERT.
LOAD DATA LOCAL INFILE '/data/products.csv'
INTO TABLE products
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS
(sku, name, price_cents, stock, @attributes)
SET attributes = CAST(@attributes AS JSON);
Envolva grandes cargas de dados em uma transação para que uma falha não deixe a tabela parcialmente importada; considere desativar índices secundários ou verificações de unicidade durante uma carga massiva pontual e, em seguida, reconstruí-los. Para gravações cotidianas da aplicação, um INSERT em lote é o padrão ideal.
Melhores práticas
- Use InnoDB para todas as tabelas e declare-o explicitamente.
- Crie bancos de dados, tabelas e conexões com
utf8mb4e uma collation consistente. - Armazene valores monetários como unidades menores em inteiros ou
DECIMAL, nunca comoFLOATouDOUBLE. - Adicione índices para as queries que você realmente executa e confirme-os com
EXPLAIN. - Mantenha o modo
ONLY_FULL_GROUP_BYpadrão e escreva queriesGROUP BYcorretas. - Use
INSERT ... ON DUPLICATE KEY UPDATEpara upserts atômicos em vez de check-then-act. - Mantenha as transações curtas, atualize as linhas em uma ordem estável e tente novamente em caso de deadlock.
- Atribua à aplicação um usuário com privilégios mínimos e mantenha o DDL em uma conta de migração.
- Faça backups consistentes com
--single-transaction, registre a posição do binlog e teste as restaurações. - Prefira keyset pagination em vez de scans extensos com
LIMIT ... OFFSET.
Erros comuns
- Manter tabelas em MyISAM e perder transações e o bloqueio a nível de linha (row-level locking).
- Usar o antigo
utf8de três bytes e depois se perguntar por que os emojis não são salvos. - Armazenar dinheiro em
FLOATe acumular erros de arredondamento. - Executar
UPDATEouDELETEsem umWHEREe reescrever a tabela inteira. - Indexar todas as colunas em vez de focar nas queries que importam, o que torna as escritas mais lentas.
- Confiar que o
AUTO_INCREMENTseja contínuo (gapless) ou significativo. - Fazer read-modify-write no código da aplicação em vez de um upsert atômico.
- Ler de uma réplica imediatamente após uma escrita e visualizar dados obsoletos (stale data).
- Desativar o
ONLY_FULL_GROUP_BYpara esconder uma agregação incorreta. - Permitir que a aplicação se conecte como
root.
Próximos passos
O MySQL e seus conceitos são amplamente aplicáveis. Se você quiser comparar com o outro principal motor relacional, leia o guia de PostgreSQL, que aborda os mesmos conceitos, mas com padrões diferentes. Para a linguagem que fundamenta ambos, o guia de SQL explora joins, CTEs e window functions a partir dos princípios básicos. E como a maioria das implantações de MySQL utiliza um cache, o Redis e o modelo de documentos do MongoDB são complementos naturais.