Tipos de Dados
Numéricos
TINYINT -- 1 byte, -128..127 (ou 0..255 UNSIGNED)
SMALLINT -- 2 bytes
MEDIUMINT -- 3 bytes
INT -- 4 bytes, -2^31..2^31-1
BIGINT -- 8 bytes, ideal para PKs de alta escala
DECIMAL(10,2)-- exato; use para dinheiro, nunca FLOAT/DOUBLE
FLOAT -- 4 bytes, impreciso
DOUBLE -- 8 bytes, impreciso
-- TINYINT(1) como booleano (MySQL não tem tipo BOOLEAN nativo)
is_active TINYINT(1) NOT NULL DEFAULT 1 -- 0=false, 1=true
-- MySQL aceita BOOLEAN como alias de TINYINT(1)Strings
CHAR(n) -- tamanho fixo, bom para hashes/CPF
VARCHAR(255) -- tamanho variável, máx 65535 bytes por linha
TINYTEXT -- até 255 bytes
TEXT -- até 65 535 bytes
MEDIUMTEXT -- até 16 MB
LONGTEXT -- até 4 GB
ENUM('pendente','pago','cancelado') -- armazena como inteiro; cuidado com ALTER
SET('leitura','escrita') -- bitmask de valoresDatas
DATE -- 'YYYY-MM-DD'
TIME -- 'HH:MM:SS'
DATETIME -- 'YYYY-MM-DD HH:MM:SS', sem timezone, 3 bytes extra para fração
TIMESTAMP -- armazenado em UTC, convertido para session tz; range 1970-2038
YEAR -- 1 byte, 1901-2155
-- frações de segundo
created_at DATETIME(6) -- microsegundos
updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)JSON e BLOB
JSON -- validado e armazenado como LONGBLOB internamente (MySQL 5.7+)
TINYBLOB -- até 255 bytes
BLOB -- até 65 535 bytes
MEDIUMBLOB -- até 16 MB
LONGBLOB -- até 4 GBComparativo MySQL vs PostgreSQL
| Conceito | MySQL 8 | PostgreSQL 16 |
|---|---|---|
| Inteiro grande | BIGINT | BIGINT / SERIAL |
| Auto-increment | AUTO_INCREMENT | SERIAL / GENERATED ALWAYS AS IDENTITY |
| Booleano nativo | TINYINT(1) | BOOLEAN |
| UUID nativo | CHAR(36) / VARCHAR(36) | UUID |
| Array | não existe | TEXT[], INT[], etc. |
| JSON | JSON / JSON_TABLE | JSON / JSONB (indexável) |
| Full-text | FULLTEXT index | tsvector / GIN |
| Enum | ENUM (DDL) | CREATE TYPE … AS ENUM |
| Intervalo de tempo | DATEDIFF, TIMEDIFF | INTERVAL nativo |
| Herança de tabelas | não suporta | TABLE INHERITANCE |
DDL
CREATE TABLE com boas práticas
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
status ENUM('pendente','pago','cancelado','reembolsado')
NOT NULL DEFAULT 'pendente',
total DECIMAL(12,2) NOT NULL,
notes TEXT,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_user (user_id),
INDEX idx_status (status),
CONSTRAINT fk_orders_users
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT,
CONSTRAINT fk_orders_products
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci;Por que
utf8mb4e naoutf8? O charsetutf8do MySQL e limitado a 3 bytes por caractere. Emojis e varios ideogramas CJK exigem 4 bytes (UTF-8 completo).utf8mb4e o UTF-8 real. Use sempreutf8mb4_0900_ai_ci(MySQL 8): accent-insensitive, case-insensitive, baseado em Unicode 9.
ALTER TABLE — ALGORITHM=INSTANT (MySQL 8)
-- Adicionar coluna sem rebuildar a tabela (in-place, sem lock)
ALTER TABLE orders
ADD COLUMN priority TINYINT NOT NULL DEFAULT 0,
ALGORITHM=INSTANT;
-- Nem toda operacao suporta INSTANT; fallback para INPLACE ou COPY
ALTER TABLE orders
MODIFY COLUMN notes MEDIUMTEXT,
ALGORITHM=INPLACE, LOCK=NONE;
-- Renomear coluna (MySQL 8+)
ALTER TABLE orders RENAME COLUMN notes TO observation;
-- Remover coluna com segurança
ALTER TABLE orders DROP COLUMN IF EXISTS legacy_field;
-- Adicionar FK após criação
ALTER TABLE orders
ADD CONSTRAINT fk_orders_products
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT;
-- ON DELETE comportamentos:
-- RESTRICT → bloqueia deleção do pai enquanto houver filhos (padrão)
-- CASCADE → deleta filhos automaticamente
-- SET NULL → zera a FK nos filhos
-- NO ACTION → semelhante a RESTRICT no InnoDBDML
INSERT
-- Multi-row (muito mais eficiente que inserts individuais)
INSERT INTO products (name, price, category)
VALUES
('Notebook', 3500.00, 'eletronicos'),
('Mouse', 89.90, 'eletronicos'),
('Cadeira', 950.00, 'moveis');
-- Ignorar conflitos de chave duplicada
INSERT IGNORE INTO users (email, name)
VALUES ('joao@example.com', 'Joao');
-- Upsert (MySQL 8.0.20+ alias syntax)
INSERT INTO orders (id, user_id, total, status)
VALUES (42, 1, 199.90, 'pendente') AS new_row
ON DUPLICATE KEY UPDATE
total = new_row.total,
status = new_row.status;
-- Upsert (sintaxe classica, ainda funciona)
INSERT INTO orders (id, user_id, total)
VALUES (42, 1, 199.90)
ON DUPLICATE KEY UPDATE
total = VALUES(total);UPDATE e DELETE com JOIN
-- UPDATE com JOIN
UPDATE orders o
JOIN users u ON u.id = o.user_id
SET o.status = 'cancelado'
WHERE u.email = 'inativo@example.com'
AND o.status = 'pendente';
-- DELETE com JOIN
DELETE o
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE u.deleted_at IS NOT NULL;
-- REPLACE INTO: faz DELETE + INSERT se a PK/UK existir
-- EVITE: zera AUTO_INCREMENT de filhos, dispara ON DELETE CASCADE
REPLACE INTO products (id, name, price) VALUES (10, 'Teclado', 150.00);
-- Workaround para RETURNING (MySQL nao tem):
INSERT INTO orders (user_id, total) VALUES (1, 299.00);
SELECT LAST_INSERT_ID(); -- retorna o id gerado
-- Ou com variavel de sessao:
INSERT INTO orders (user_id, total) VALUES (1, 299.00);
SET @new_id = LAST_INSERT_ID();
SELECT * FROM orders WHERE id = @new_id;SELECT e Filtering
-- Basico com alias (backticks para nomes reservados)
SELECT
u.id,
u.name AS `nome_completo`,
COUNT(o.id) AS total_pedidos,
COALESCE(SUM(o.total), 0.00) AS valor_total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at >= '2024-01-01'
AND u.status = 'ativo'
GROUP BY u.id, u.name
HAVING total_pedidos > 0
ORDER BY valor_total DESC
LIMIT 20 OFFSET 0;
-- Operadores uteis
WHERE price BETWEEN 100 AND 500
WHERE status IN ('pago', 'pendente')
WHERE name LIKE 'Jo%' -- prefixo (usa indice)
WHERE name LIKE '%ao' -- sufixo (full scan)
WHERE description REGEXP '^[A-Z]'
WHERE metadata IS NOT NULL
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'pago'
)
-- Subquery correlacionada
SELECT p.name,
(SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id) AS vendas
FROM products p
ORDER BY vendas DESC;JOINs
-- INNER JOIN
SELECT u.name, o.id, o.total
FROM users u
INNER JOIN orders o ON o.user_id = u.id;
-- LEFT JOIN (inclui usuarios sem pedidos)
SELECT u.name, COUNT(o.id) AS pedidos
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id;
-- RIGHT JOIN (raro — prefira reescrever como LEFT JOIN)
SELECT o.id, u.name
FROM orders o
RIGHT JOIN users u ON u.id = o.user_id;
-- FULL OUTER JOIN (MySQL nao tem; simule com UNION)
SELECT u.id, u.name, o.id AS order_id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
UNION
SELECT u.id, u.name, o.id
FROM users u
RIGHT JOIN orders o ON o.user_id = u.id;
-- CROSS JOIN
SELECT u.name, p.name
FROM users u CROSS JOIN products p;
-- SELF JOIN (hierarquia de usuarios)
SELECT e.name AS funcionario, m.name AS gestor
FROM users e
LEFT JOIN users m ON m.id = e.manager_id;
-- Hints de join
SELECT /*+ STRAIGHT_JOIN */ u.name, o.total
FROM users u
STRAIGHT_JOIN orders o ON o.user_id = u.id; -- forca ordem das tabelas
-- Hints de indice
SELECT * FROM orders USE INDEX (idx_status) WHERE status = 'pago';
SELECT * FROM orders FORCE INDEX (idx_user) WHERE user_id = 42;
SELECT * FROM orders IGNORE INDEX (PRIMARY) WHERE id = 1;Window Functions (MySQL 8+)
-- ROW_NUMBER, RANK, DENSE_RANK
SELECT
user_id,
total,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY total DESC) AS rn,
RANK() OVER (PARTITION BY user_id ORDER BY total DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY user_id ORDER BY total DESC) AS dense_rnk
FROM orders;
-- Pegar o maior pedido por usuario (top-1 por grupo)
SELECT user_id, total
FROM (
SELECT user_id, total,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY total DESC) AS rn
FROM orders
) ranked
WHERE rn = 1;
-- LAG e LEAD (acesso a linha anterior/seguinte)
SELECT
id,
total,
LAG(total) OVER (ORDER BY created_at) AS pedido_anterior,
LEAD(total) OVER (ORDER BY created_at) AS proximo_pedido,
total - LAG(total) OVER (ORDER BY created_at) AS variacao
FROM orders
WHERE user_id = 1;
-- NTILE (percentis)
SELECT id, total,
NTILE(4) OVER (ORDER BY total) AS quartil
FROM orders;
-- FIRST_VALUE / LAST_VALUE
SELECT
id,
total,
FIRST_VALUE(total) OVER w AS primeiro,
LAST_VALUE(total) OVER w AS ultimo
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY created_at
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
-- SUM / AVG acumulado
SELECT
created_at,
total,
SUM(total) OVER (ORDER BY created_at ROWS UNBOUNDED PRECEDING) AS acumulado,
AVG(total) OVER (ORDER BY created_at ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS media_7
FROM orders
WHERE user_id = 1;CTEs (MySQL 8+)
-- CTE simples
WITH pedidos_pagos AS (
SELECT user_id, SUM(total) AS total_pago
FROM orders
WHERE status = 'pago'
GROUP BY user_id
)
SELECT u.name, pp.total_pago
FROM users u
JOIN pedidos_pagos pp ON pp.user_id = u.id
ORDER BY pp.total_pago DESC;
-- CTEs encadeadas
WITH
base AS (
SELECT user_id, total FROM orders WHERE status = 'pago'
),
agregado AS (
SELECT user_id, SUM(total) AS soma, COUNT(*) AS qtd
FROM base
GROUP BY user_id
),
top_users AS (
SELECT user_id FROM agregado WHERE soma > 1000
)
SELECT u.name, a.soma, a.qtd
FROM users u
JOIN agregado a ON a.user_id = u.id
JOIN top_users t ON t.user_id = u.id;
-- CTE recursiva (hierarquia de categorias)
WITH RECURSIVE categoria_tree AS (
-- ancora: raizes
SELECT id, name, parent_id, 0 AS depth, CAST(name AS CHAR(500)) AS path
FROM categories
WHERE parent_id IS NULL
UNION ALL
-- recursao
SELECT c.id, c.name, c.parent_id, ct.depth + 1,
CONCAT(ct.path, ' > ', c.name)
FROM categories c
JOIN categoria_tree ct ON ct.id = c.parent_id
)
SELECT * FROM categoria_tree ORDER BY path;
-- CTE vs alternativas em MySQL
-- CTE → legibilidade; MySQL NESTE materializa CTEs (pode ser mais lento)
-- Subquery → pode ser otimizada melhor; menos legivel
-- Temp table → util para reutilizar resultado em multiplas queries da session
CREATE TEMPORARY TABLE tmp_pagos AS
SELECT user_id, SUM(total) AS soma FROM orders WHERE status='pago' GROUP BY user_id;JSON (MySQL 5.7+)
-- Coluna JSON
ALTER TABLE users ADD COLUMN preferences JSON;
-- Inserir JSON
UPDATE users
SET preferences = '{"theme":"dark","lang":"pt","notifications":true}'
WHERE id = 1;
-- -> retorna JSON; ->> retorna string (sem aspas)
SELECT preferences->'$.theme' AS theme_json, -- "dark"
preferences->>'$.theme' AS theme_string -- dark
FROM users WHERE id = 1;
-- Equivalente com funcao
SELECT JSON_EXTRACT(preferences, '$.theme') FROM users WHERE id = 1;
SELECT JSON_UNQUOTE(JSON_EXTRACT(preferences, '$.theme')) FROM users WHERE id = 1;
-- Modificar
UPDATE users SET preferences = JSON_SET(preferences, '$.lang', 'en') WHERE id = 1;
UPDATE users SET preferences = JSON_INSERT(preferences, '$.beta', true) WHERE id = 1;
UPDATE users SET preferences = JSON_REMOVE(preferences, '$.beta') WHERE id = 1;
UPDATE users SET preferences = JSON_REPLACE(preferences, '$.theme','light') WHERE id = 1;
-- JSON_TABLE — transforma JSON em linhas (MySQL 8)
SELECT jt.*
FROM orders,
JSON_TABLE(
items,
'$[*]' COLUMNS (
product_id INT PATH '$.product_id',
qty INT PATH '$.qty',
price DECIMAL(10,2) PATH '$.price'
)
) AS jt;
-- Indice gerado em campo JSON
ALTER TABLE users
ADD COLUMN pref_theme VARCHAR(20)
GENERATED ALWAYS AS (preferences->>'$.theme') STORED,
ADD INDEX idx_pref_theme (pref_theme);
-- Multi-valued index (MySQL 8.0.17+) com MEMBER OF
ALTER TABLE products ADD INDEX idx_tags ((CAST(tags AS UNSIGNED ARRAY)));
SELECT * FROM products WHERE 5 MEMBER OF (tags);Indices
-- B-tree (padrao InnoDB)
CREATE INDEX idx_status_created ON orders (status, created_at);
-- Unique
CREATE UNIQUE INDEX uq_users_email ON users (email);
-- FULLTEXT (para busca textual)
CREATE FULLTEXT INDEX ft_products_name ON products (name, description);
SELECT * FROM products
WHERE MATCH(name, description) AGAINST ('notebook gamer' IN BOOLEAN MODE);
-- SPATIAL (requer coluna GEOMETRY/POINT)
CREATE SPATIAL INDEX idx_location ON stores (coordinates);
-- Functional index (MySQL 8+) — indexa expressao
CREATE INDEX idx_lower_email ON users ((LOWER(email)));
SELECT * FROM users WHERE LOWER(email) = 'joao@example.com'; -- usa o indice
-- INVISIBLE index (testa remocao sem dropar)
ALTER TABLE orders ALTER INDEX idx_status INVISIBLE;
-- SET SESSION optimizer_switch='use_invisible_indexes=on'; para testar
-- Covering index (inclui todas as colunas da query)
CREATE INDEX idx_cover_orders ON orders (user_id, status, total);
-- Query usa apenas o indice, sem acessar a tabela (Extra: Using index)
-- InnoDB clustered index: dados fisicamente ordenados pela PK
-- Secondary indexes contem o valor da PK como ponteiro → PKs pequenas sao melhores
-- Leftmost prefix rule
-- Indice (a, b, c) serve para: (a), (a,b), (a,b,c) mas NAO (b) ou (b,c)
CREATE INDEX idx_multi ON orders (user_id, status, created_at);
-- Funciona: WHERE user_id=1
-- Funciona: WHERE user_id=1 AND status='pago'
-- NAO usa: WHERE status='pago'
-- EXPLAIN basico
EXPLAIN SELECT * FROM orders WHERE user_id = 1 AND status = 'pago';
-- EXPLAIN com JSON detalhado
EXPLAIN FORMAT=JSON
SELECT o.id, u.name
FROM orders o JOIN users u ON u.id = o.user_id
WHERE o.status = 'pago'\GEXPLAIN e Performance
-- Colunas chave do EXPLAIN:
-- type: ALL < index < range < ref < eq_ref < const < system (melhor = direita)
-- key: indice usado
-- rows: estimativa de linhas examinadas
-- Extra: Using filesort, Using temporary, Using index (covering)
-- EXPLAIN ANALYZE (MySQL 8.0.18+) — executa e mostra tempo real
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id)
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id\G
-- Ativar slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- segundos
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- performance_schema: top queries por tempo total
SELECT digest_text, count_star, avg_timer_wait/1e9 AS avg_ms,
sum_timer_wait/1e9 AS total_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC
LIMIT 10;
-- Queries em execucao agora
SELECT * FROM performance_schema.events_statements_current
WHERE sql_text IS NOT NULL\G
-- sys schema (MySQL 5.7.7+) — vistas prontas
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;
SELECT * FROM sys.schema_unused_indexes;
SELECT * FROM sys.schema_redundant_indexes;
SELECT * FROM sys.innodb_buffer_stats_by_table;Stored Procedures e Funcoes
DELIMITER $$
-- Procedure com IN/OUT e tratamento de erro
CREATE PROCEDURE processar_pedido(
IN p_order_id BIGINT,
IN p_user_id BIGINT,
OUT p_resultado VARCHAR(100)
)
proc_label: BEGIN
DECLARE v_total DECIMAL(12,2);
DECLARE v_status VARCHAR(20);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_resultado = 'ERRO: falha ao processar pedido';
END;
START TRANSACTION;
SELECT total, status INTO v_total, v_status
FROM orders
WHERE id = p_order_id AND user_id = p_user_id
FOR UPDATE;
IF v_status != 'pendente' THEN
ROLLBACK;
SET p_resultado = 'ERRO: pedido nao esta pendente';
LEAVE proc_label;
END IF;
UPDATE orders SET status = 'pago' WHERE id = p_order_id;
COMMIT;
SET p_resultado = CONCAT('OK: pedido ', p_order_id, ' pago, total=', v_total);
END$$
-- Chamada
CALL processar_pedido(42, 1, @res);
SELECT @res;$$
-- Cursor (iterar linhas)
CREATE PROCEDURE atualizar_status_em_lote()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE v_id BIGINT;
DECLARE cur CURSOR FOR
SELECT id FROM orders WHERE status = 'pendente' AND created_at < NOW() - INTERVAL 30 DAY;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
loop_label: LOOP
FETCH cur INTO v_id;
IF done THEN LEAVE loop_label; END IF;
UPDATE orders SET status = 'cancelado' WHERE id = v_id;
END LOOP;
CLOSE cur;
END$$
-- Funcao deterministica
CREATE FUNCTION calcular_desconto(
p_total DECIMAL(12,2),
p_category VARCHAR(50)
) RETURNS DECIMAL(12,2)
DETERMINISTIC
BEGIN
DECLARE v_pct DECIMAL(5,2) DEFAULT 0;
IF p_category = 'eletronicos' THEN SET v_pct = 0.05;
ELSEIF p_total > 1000 THEN SET v_pct = 0.10;
END IF;
RETURN ROUND(p_total * v_pct, 2);
END$$
DELIMITER ;
-- Quando usar stored procedures:
-- Bom: logica de negocio atomica, batch jobs, encapsular permissoes
-- Evitar: logica complexa que muda frequentemente (deploy = ALTER PROCEDURE);
-- testes unitarios sao dificeis; debugging limitadoTransacoes
-- SAVEPOINT
START TRANSACTION;
INSERT INTO orders (user_id, total) VALUES (1, 100.00);
SAVEPOINT sp1;
INSERT INTO orders (user_id, total) VALUES (2, 200.00);
-- erro detectado: desfaz apenas desde sp1
ROLLBACK TO SAVEPOINT sp1;
-- primeira insercao ainda esta na transacao
COMMIT;
-- SELECT FOR UPDATE (lock exclusivo)
START TRANSACTION;
SELECT * FROM orders WHERE id = 42 FOR UPDATE;
UPDATE orders SET status = 'pago' WHERE id = 42;
COMMIT;
-- FOR SHARE (MySQL 8) — lock compartilhado, permite outros FOR SHARE
SELECT * FROM products WHERE id = 10 FOR SHARE;
-- SKIP LOCKED — processar fila sem bloquear (pattern job queue)
START TRANSACTION;
SELECT * FROM orders
WHERE status = 'pendente'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;
-- NOWAIT — falha imediatamente se nao conseguir lock
SELECT * FROM orders WHERE id = 42 FOR UPDATE NOWAIT;
-- Isolation levels
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- READ UNCOMMITTED → dirty reads
-- READ COMMITTED → padrao PostgreSQL; sem dirty reads
-- REPEATABLE READ → padrao MySQL/InnoDB; phantom reads possiveis
-- SERIALIZABLE → mais restritivo; serializa totalmente
-- Prevenir deadlocks:
-- 1. Sempre adquirir locks na mesma ordem
-- 2. Usar transacoes curtas
-- 3. Indice nas colunas filtradas no FOR UPDATE (sem indice = table lock)
-- 4. Monitorar: SHOW ENGINE INNODB STATUS\G → secao LATEST DETECTED DEADLOCKReplicacao
Binlog
-- Verificar formato do binlog
SHOW VARIABLES LIKE 'binlog_format';
-- STATEMENT: replica SQL (menor volume, pode ser nao-deterministico)
-- ROW: replica dados alterados linha a linha (mais seguro, maior volume)
-- MIXED: usa STATEMENT por padrao, ROW quando necessario (recomendado)
-- Ver posicao atual do binlog
SHOW MASTER STATUS;
SHOW BINARY LOGS;
SHOW BINLOG EVENTS IN 'mysql-bin.000001' LIMIT 20;GTID (Global Transaction ID)
# my.cnf — habilitar GTID
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_format = ROW
log_replica_updates = ON # MySQL 8 (era log_slave_updates)-- Status do replica (MySQL 8 — terminologia atualizada)
SHOW REPLICA STATUS\G
-- Replica_IO_Running: Yes
-- Replica_SQL_Running: Yes
-- Configurar replica com GTID (MySQL 8)
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='primary.host',
SOURCE_USER='repl_user',
SOURCE_PASSWORD='secret',
SOURCE_AUTO_POSITION=1;
START REPLICA;Group Replication / InnoDB Cluster
-- InnoDB Cluster gerenciado pelo MySQL Shell
-- mysqlsh> dba.createCluster('meuCluster')
-- mysqlsh> cluster.addInstance('user@host2:3306')
-- mysqlsh> cluster.status()
-- ProxySQL para read/write split
-- Regras:
-- SELECT → hostgroup de replicas (leitura)
-- INSERT/UPDATE/DELETE/BEGIN → hostgroup primario (escrita)Administracao
Usuarios e Permissoes
-- Criar usuario
CREATE USER 'app_user'@'%' IDENTIFIED BY 'senha_forte';
CREATE USER 'readonly'@'10.0.0.%' IDENTIFIED BY 'outra_senha';
-- Conceder permissoes
GRANT SELECT, INSERT, UPDATE, DELETE ON financeiro.* TO 'app_user'@'%';
GRANT SELECT ON financeiro.* TO 'readonly'@'10.0.0.%';
GRANT ALL PRIVILEGES ON financeiro.* TO 'admin_user'@'localhost';
-- Revogar
REVOKE INSERT, UPDATE ON financeiro.orders FROM 'app_user'@'%';
-- Roles (MySQL 8+)
CREATE ROLE 'app_read', 'app_write';
GRANT SELECT ON financeiro.* TO 'app_read';
GRANT INSERT, UPDATE, DELETE ON financeiro.* TO 'app_write';
GRANT 'app_read', 'app_write' TO 'app_user'@'%';
SET DEFAULT ROLE ALL TO 'app_user'@'%';
FLUSH PRIVILEGES; -- necessario apos manipulacao direta em mysql.user (raro)
-- Ver permissoes
SHOW GRANTS FOR 'app_user'@'%';Backup e Restore
# mysqldump — backup logico
mysqldump -u root -p --single-transaction --routines --triggers \
financeiro > financeiro_$(date +%F).sql
# Restaurar
mysql -u root -p financeiro < financeiro_2024-01-01.sql
# MySQL Shell dump — paralelo, comprimido (recomendado para producao)
mysqlsh root@localhost -- util dumpSchemas financeiro \
--outputUrl=/backup/financeiro --threads=4
mysqlsh root@localhost -- util loadDump /backup/financeiro \
--threads=4 --resetProgress
# Percona XtraBackup — backup fisico online (InnoDB)
xtrabackup --backup --target-dir=/backup/full --user=root --password=secret
xtrabackup --prepare --target-dir=/backup/fullVariaveis criticas
-- Ver variaveis
SHOW VARIABLES LIKE 'innodb_buffer_pool%';
SHOW VARIABLES LIKE 'max_connections';
-- Variaveis criticas de performance
-- innodb_buffer_pool_size: 70-80% da RAM disponivel para o MySQL
-- innodb_buffer_pool_instances: 1 por GB de buffer pool, max 8-16
-- innodb_log_file_size: 1-2 GB para workloads de escrita intensa
-- max_connections: ajustar com pool de conexoes (PgBouncer equivalente: ProxySQL)
-- query_cache_size: REMOVIDO no MySQL 8.0 (era nocivo em alta concorrencia)
-- tmp_table_size / max_heap_table_size: 64M+ para queries com GROUP BY complexo
-- Alterar em runtime (persistir com SET PERSIST no MySQL 8)
SET PERSIST innodb_buffer_pool_size = 2147483648; -- 2 GBCLI e monitoramento
-- Listar processos
SHOW PROCESSLIST;
-- Ou mais detalhado:
SELECT * FROM information_schema.PROCESSLIST WHERE COMMAND != 'Sleep';
-- Matar query especifica
KILL QUERY 42; -- cancela a query, mantém conexão
KILL 42; -- encerra a conexão
-- CLI atalhos (mysql client)
-- \G → resultado vertical (melhor para linhas largas)
-- source /path/to/file.sql → executar arquivo
-- \! ls → comando shell
-- \s → status da conexao
-- information_schema queries uteis
SELECT table_name, table_rows,
ROUND(data_length/1024/1024, 2) AS data_mb,
ROUND(index_length/1024/1024, 2) AS index_mb
FROM information_schema.TABLES
WHERE table_schema = 'financeiro'
ORDER BY data_length + index_length DESC;
-- sys schema
SELECT * FROM sys.schema_table_lock_waits;
SELECT * FROM sys.innodb_lock_waits;
SELECT host, total_connections, current_connections
FROM sys.host_summary;MySQL vs PostgreSQL
| Criterio | MySQL 8 | PostgreSQL 16 |
|---|---|---|
| Licenca | GPL (Oracle) / Comercial | PostgreSQL License (permissiva) |
| Arquitetura | Multi-engine (InnoDB, MyISAM) | Single engine, extensivel |
| ACID | Sim (InnoDB) | Sim (padrao) |
| JSONB / JSON indexavel | JSON (sem binario real) | JSONB com GIN (superior) |
| Arrays nativos | Nao | Sim (tipo nativo, indexavel) |
| Full-text search | FULLTEXT basico | tsvector/tsquery robusto |
| Window functions | Sim (8.0+) | Sim (ha mais tempo, mais maduro) |
| CTEs materializadas | Sempre materializa | Controlavel (MATERIALIZED / NOT MATERIALIZED) |
| Particionamento | Sim (RANGE, LIST, HASH, KEY) | Sim (RANGE, LIST, HASH) — mais flexivel |
| Replicacao logica | Sim (binlog row) | Logical replication nativa (publicacao/subscricao) |
| Extensibilidade | Limitada | Alta (PostGIS, pgvector, TimescaleDB…) |
| Upsert | ON DUPLICATE KEY UPDATE | INSERT … ON CONFLICT DO UPDATE |
| RETURNING | Nao (usar LAST_INSERT_ID) | Sim (INSERT/UPDATE/DELETE RETURNING) |
| Concorrencia / MVCC | Bom (InnoDB) | Excelente (VACUUM cuida de bloat) |
| Ecossistema / adocao | Muito alto (web/SaaS) | Crescendo, forte em analitico |
Quando escolher MySQL
- Stack LAMP/LEMP ja estabelecida
- Equipe familiar com MySQL
- Workloads de leitura intensa com replicas simples
- Ferramentas gerenciadas (RDS MySQL, PlanetScale, Vitess)
Quando escolher PostgreSQL
- Necessidade de tipos avancados (arrays, ranges, jsonb, uuid nativo)
- Extensoes geograficas (PostGIS) ou vetoriais (pgvector)
- Logica complexa no banco (funcoes, triggers avancadas)
- Conformidade SQL mais estrita
- Dados analiticos com CTEs e window functions complexas
Mapeamento de tipos para migracao
| MySQL | PostgreSQL |
|---|---|
| TINYINT(1) | BOOLEAN |
| AUTO_INCREMENT | SERIAL / GENERATED ALWAYS AS IDENTITY |
| DATETIME | TIMESTAMP |
| ENUM(‘a’,‘b’) | CREATE TYPE t AS ENUM(‘a’,‘b’) |
| TEXT / LONGTEXT | TEXT |
| DOUBLE | DOUBLE PRECISION |
| UNSIGNED INT | INTEGER (sem unsigned; use CHECK > 0) |
| JSON | JSONB |
| BLOB | BYTEA |
| ON UPDATE CURRENT_TIMESTAMP | trigger com NOW() |
Semi-Sync Replication
Na replicação assíncrona padrão do MySQL o primário confirma o commit sem esperar nenhuma réplica: alta performance, mas qualquer crash imediatamente após o commit pode fazer o dado desaparecer (o binlog ainda não chegou à réplica).
Na replicação síncrona completa o primário só confirma depois que todas as réplicas persistiram o evento: zero perda de dados, mas latência proporcional à réplica mais lenta.
A replicação semi-síncrona fica no meio: o primário aguarda ACK de pelo menos uma réplica antes de retornar ao cliente. Se nenhuma réplica responder dentro do timeout configurado, o MySQL faz fallback para modo assíncrono automaticamente — garantindo disponibilidade com risco mínimo de perda.
Instalação do plugin
-- No primário
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 10000; -- ms; cai para async após 10 s sem ACK
-- Em cada réplica
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
-- Reinicia o thread de I/O para ativar
STOP SLAVE IO_THREAD;
START SLAVE IO_THREAD;Para persistir entre reinicializações, adicione em my.cnf:
[mysqld]
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_timeout = 10000Variáveis importantes
| Variável | Descrição |
|---|---|
rpl_semi_sync_master_enabled | Liga/desliga no primário |
rpl_semi_sync_master_timeout | Timeout (ms) antes do fallback para async |
rpl_semi_sync_master_wait_no_slave | Se ON, segura o commit mesmo sem réplicas conectadas |
rpl_semi_sync_slave_enabled | Liga/desliga na réplica |
Monitoramento
-- Verifica estado geral (primário)
SHOW STATUS LIKE 'Rpl_semi_sync%';
-- Rpl_semi_sync_master_status → ON = ativo, OFF = caiu para async
-- Rpl_semi_sync_master_yes_tx → total de commits confirmados semi-sync
-- Rpl_semi_sync_master_no_tx → total de commits que caíram para async
-- Rpl_semi_sync_master_clients → réplicas conectadas com semi-sync ativo
-- Rpl_semi_sync_master_tx_avg_wait_time → latência média aguardando ACK (µs)
-- Na réplica
SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; -- ON = conectada e ativaGroup Replication
O Group Replication (GR) é a solução nativa do MySQL para alta disponibilidade com tolerância a falhas. Em vez de um primário simples enviando binlog para réplicas passivas, todos os nós formam um grupo e chegam a acordo sobre a ordem de transações usando um protocolo de consenso semelhante ao Paxos (implementação interna chamada XCom).
Pré-requisitos obrigatórios
-- Todas as tabelas devem usar InnoDB
-- Todas as tabelas devem ter uma PRIMARY KEY explícita
-- binlog_format = ROW obrigatório
-- GTID obrigatório
-- Verificar tabelas sem PK
SELECT t.table_schema, t.table_name
FROM information_schema.tables t
LEFT JOIN information_schema.table_constraints c
ON t.table_schema = c.table_schema
AND t.table_name = c.table_name
AND c.constraint_type = 'PRIMARY KEY'
WHERE t.table_type = 'BASE TABLE'
AND t.table_schema NOT IN ('mysql','information_schema','performance_schema','sys')
AND c.constraint_name IS NULL;my.cnf — configuração mínima por nó
[mysqld]
server_id = 1 # único por nó
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_format = ROW
log_bin = ON
log_replica_updates = ON
plugin_load_add = group_replication.so
group_replication_group_name = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" # UUID fixo do grupo
group_replication_start_on_boot = OFF
group_replication_local_address = "nó1:33061"
group_replication_group_seeds = "nó1:33061,nó2:33061,nó3:33061"
group_replication_bootstrap_group = OFFBootstrap do primeiro nó
-- Apenas uma vez, no primeiro nó
SET GLOBAL group_replication_bootstrap_group = ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group = OFF;
-- Verificar
SELECT * FROM performance_schema.replication_group_members;Adicionar membros ao grupo
-- Nos demais nós (sem bootstrap)
START GROUP_REPLICATION;
-- Acompanhar sincronização
SELECT member_id, member_host, member_port, member_state, member_role
FROM performance_schema.replication_group_members;
-- member_state: ONLINE = pronto | RECOVERING = sincronizando | ERRORSingle-Primary vs Multi-Primary
-- SINGLE-PRIMARY (padrão recomendado)
-- Um nó é o primário (escritas); os demais são secundários somente leitura.
-- Failover automático: se o primário cai, o grupo elege um novo.
SET GLOBAL group_replication_single_primary_mode = ON;
-- Verificar quem é o primário
SELECT member_host, member_role
FROM performance_schema.replication_group_members
WHERE member_role = 'PRIMARY';
-- MULTI-PRIMARY
-- Qualquer nó aceita escritas. Requer cuidado com conflitos de atualização
-- (o GR detecta e faz rollback no conflito — a taxa de conflito deve ser baixa).
SET GLOBAL group_replication_single_primary_mode = OFF;
SET GLOBAL group_replication_enforce_update_everywhere_checks = ON;Group Replication vs Orchestrator + replicação assíncrona
| Critério | Group Replication | Orchestrator + async |
|---|---|---|
| Failover automático | Nativo (XCom consensus) | Sim (Orchestrator detecta e promove) |
| Perda de dados (RPO) | Praticamente zero (certificação no commit) | Depende do lag da réplica |
| Complexidade de setup | Média-alta | Menor (replicação clássica + Orchestrator) |
| Tolerância a partições | Quórum obrigatório (maioria dos nós) | Mais flexível |
| Multi-primary | Suportado (com cuidado) | Não nativo |
| Indicado para | HA crítico, RPO ≈ 0 | Infraestrutura já com replicação async |
MySQL InnoDB Cluster e MySQL Router
O InnoDB Cluster é o stack oficial de HA do MySQL composto por três camadas:
┌──────────────────────────────────────────────────────────┐
│ Aplicação │
│ ↓ │
│ MySQL Router (proxy local, failover transparente) │
│ ↓ │
│ InnoDB Cluster (Group Replication gerenciado) │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Nó 1 │ │ Nó 2 │ │ Nó 3 │ │
│ │ (primary) │ │(secondary)│ │(secondary)│ │
│ └───────────┘ └───────────┘ └───────────┘ │
└──────────────────────────────────────────────────────────┘
Gerenciamento via MySQL Shell (AdminAPI)Criando o cluster com MySQL Shell
// MySQL Shell (mysqlsh)
// Conectar ao nó primário
\connect root@localhost:3306
// Verificar se o nó está pronto para o cluster
dba.checkInstanceConfiguration('root@localhost:3306')
// Criar o cluster (bootstraps o Group Replication internamente)
var cluster = dba.createCluster('MeuCluster')
// Adicionar réplicas
cluster.addInstance('root@nó2:3306')
cluster.addInstance('root@nó3:3306')
// Status completo
cluster.status()
// {
// "clusterName": "MeuCluster",
// "defaultReplicaSet": {
// "status": "OK",
// "topology": {
// "nó1:3306": { "mode": "R/W", "status": "ONLINE" },
// "nó2:3306": { "mode": "R/O", "status": "ONLINE" },
// "nó3:3306": { "mode": "R/O", "status": "ONLINE" }
// }
// }
// }MySQL Router — proxy com failover automático
O MySQL Router é instalado próximo à aplicação (idealmente no mesmo host). Ele consulta os metadados do cluster e roteia automaticamente:
# Bootstrap do router contra o cluster
mysqlrouter --bootstrap root@nó1:3306 --directory /etc/mysqlrouter --user mysqlrouter
# Inicia
mysqlrouter --config /etc/mysqlrouter/mysqlrouter.conf &Portas padrão após bootstrap:
| Porta | Comportamento |
|---|---|
| 6446 | Leitura/escrita → sempre o primário |
| 6447 | Somente leitura → distribui entre secundários |
| 6448 | Leitura/escrita (protocolo X) |
| 6449 | Somente leitura (protocolo X) |
// A aplicação aponta para o Router, não para os nós diretamente
spring.datasource.url=jdbc:mysql://localhost:6446/meu_banco
// Se o primário falhar, o Router detecta em segundos e redireciona para o novo primário.
// A aplicação reconecta automaticamente — zero mudança de configuração.InnoDB Cluster vs Replicação manual
| InnoDB Cluster | Replicação manual + Orchestrator | |
|---|---|---|
| Setup inicial | MySQL Shell AdminAPI (5–10 min) | Mais etapas (GTID, binlog, Orchestrator) |
| Failover | Automático e gerenciado | Automático via Orchestrator, porém externo |
| Monitoramento | cluster.status() unificado | Múltiplas ferramentas |
| Flexibilidade | Menor (exige AdminAPI para mudanças) | Maior controle manual |
| Suporte Oracle | Oficial | Comunidade |
| Indicado para | Novos projetos que querem HA com mínimo de ops | Ambientes legados ou com requisitos específicos |