Data

MySQL

Referência completa de MySQL 8+ — tipos, queries, índices, window functions, JSON, stored procedures, replicação e administração

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 valores

Datas

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 GB

Comparativo MySQL vs PostgreSQL

ConceitoMySQL 8PostgreSQL 16
Inteiro grandeBIGINTBIGINT / SERIAL
Auto-incrementAUTO_INCREMENTSERIAL / GENERATED ALWAYS AS IDENTITY
Booleano nativoTINYINT(1)BOOLEAN
UUID nativoCHAR(36) / VARCHAR(36)UUID
Arraynão existeTEXT[], INT[], etc.
JSONJSON / JSON_TABLEJSON / JSONB (indexável)
Full-textFULLTEXT indextsvector / GIN
EnumENUM (DDL)CREATE TYPE … AS ENUM
Intervalo de tempoDATEDIFF, TIMEDIFFINTERVAL nativo
Herança de tabelasnão suportaTABLE 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 utf8mb4 e nao utf8? O charset utf8 do MySQL e limitado a 3 bytes por caractere. Emojis e varios ideogramas CJK exigem 4 bytes (UTF-8 completo). utf8mb4 e o UTF-8 real. Use sempre utf8mb4_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 InnoDB

DML

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'\G

EXPLAIN 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 limitado

Transacoes

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

Replicacao

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/full

Variaveis 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 GB

CLI 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

CriterioMySQL 8PostgreSQL 16
LicencaGPL (Oracle) / ComercialPostgreSQL License (permissiva)
ArquiteturaMulti-engine (InnoDB, MyISAM)Single engine, extensivel
ACIDSim (InnoDB)Sim (padrao)
JSONB / JSON indexavelJSON (sem binario real)JSONB com GIN (superior)
Arrays nativosNaoSim (tipo nativo, indexavel)
Full-text searchFULLTEXT basicotsvector/tsquery robusto
Window functionsSim (8.0+)Sim (ha mais tempo, mais maduro)
CTEs materializadasSempre materializaControlavel (MATERIALIZED / NOT MATERIALIZED)
ParticionamentoSim (RANGE, LIST, HASH, KEY)Sim (RANGE, LIST, HASH) — mais flexivel
Replicacao logicaSim (binlog row)Logical replication nativa (publicacao/subscricao)
ExtensibilidadeLimitadaAlta (PostGIS, pgvector, TimescaleDB…)
UpsertON DUPLICATE KEY UPDATEINSERT … ON CONFLICT DO UPDATE
RETURNINGNao (usar LAST_INSERT_ID)Sim (INSERT/UPDATE/DELETE RETURNING)
Concorrencia / MVCCBom (InnoDB)Excelente (VACUUM cuida de bloat)
Ecossistema / adocaoMuito 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

MySQLPostgreSQL
TINYINT(1)BOOLEAN
AUTO_INCREMENTSERIAL / GENERATED ALWAYS AS IDENTITY
DATETIMETIMESTAMP
ENUM(‘a’,‘b’)CREATE TYPE t AS ENUM(‘a’,‘b’)
TEXT / LONGTEXTTEXT
DOUBLEDOUBLE PRECISION
UNSIGNED INTINTEGER (sem unsigned; use CHECK > 0)
JSONJSONB
BLOBBYTEA
ON UPDATE CURRENT_TIMESTAMPtrigger 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 = 10000

Variáveis importantes

VariávelDescrição
rpl_semi_sync_master_enabledLiga/desliga no primário
rpl_semi_sync_master_timeoutTimeout (ms) antes do fallback para async
rpl_semi_sync_master_wait_no_slaveSe ON, segura o commit mesmo sem réplicas conectadas
rpl_semi_sync_slave_enabledLiga/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 ativa

Group 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 = OFF

Bootstrap 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 | ERROR

Single-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érioGroup ReplicationOrchestrator + async
Failover automáticoNativo (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 setupMédia-altaMenor (replicação clássica + Orchestrator)
Tolerância a partiçõesQuórum obrigatório (maioria dos nós)Mais flexível
Multi-primarySuportado (com cuidado)Não nativo
Indicado paraHA crítico, RPO ≈ 0Infraestrutura 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:

PortaComportamento
6446Leitura/escrita → sempre o primário
6447Somente leitura → distribui entre secundários
6448Leitura/escrita (protocolo X)
6449Somente 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 ClusterReplicação manual + Orchestrator
Setup inicialMySQL Shell AdminAPI (5–10 min)Mais etapas (GTID, binlog, Orchestrator)
FailoverAutomático e gerenciadoAutomático via Orchestrator, porém externo
Monitoramentocluster.status() unificadoMúltiplas ferramentas
FlexibilidadeMenor (exige AdminAPI para mudanças)Maior controle manual
Suporte OracleOficialComunidade
Indicado paraNovos projetos que querem HA com mínimo de opsAmbientes legados ou com requisitos específicos