Dia 9 — Banco de dados relacional: PHP e MySQL
Banco, tabela, linha e coluna
Banco de dados, SGBD e esquema
Banco de dados é o conjunto dos dados guardados; SGBD é o programa que cria as estruturas, impõe as regras e responde às consultas. No curso o SGBD é o MySQL e o banco se chama escola. O conjunto de tabelas com as regras que o banco mantém é o esquema.
Comandos de administração
| Comando | O que faz |
|---|---|
CREATE DATABASE escola | Cria o banco; com IF NOT EXISTS não dá erro se ele já existir |
USE escola | Escolhe o banco das consultas seguintes |
SHOW DATABASES | Lista os bancos do servidor |
SHOW TABLES | Lista as tabelas do banco atual |
DESCRIBE alunos | Mostra coluna, tipo, se aceita nulo e chave de cada uma |
SHOW CREATE TABLE alunos | Mostra o comando completo que gerou a tabela |
DROP DATABASE escola | Apaga o banco inteiro, com todas as tabelas |
Tabela, linha e coluna
A tabela agrupa fatos do mesmo tipo, a linha é cada fato e a coluna é cada
característica desse fato. Na tabela alunos, a linha é uma pessoa e as colunas
são nome, e-mail, cidade e nascimento. O tipo é decidido na criação da tabela e
vale para todas as linhas: em uma coluna VARCHAR(120) o banco corta o
centésimo vigésimo primeiro caractere de qualquer nome, sempre.
Sinônimos que aparecem em material antigo
| Termo | Sinônimo | Onde aparece na consulta |
|---|---|---|
| Tabela | Entidade, relação | FROM alunos |
| Coluna | Campo, atributo | SELECT nome |
| Linha | Registro, tupla | Uma linha do resultado |
| Chave | Índice identificador | PRIMARY KEY (id) |
Chave primária e chave estrangeira
A chave primária identifica a linha de forma única: no MySQL nunca aceita NULL e
nunca repete valor. A chave estrangeira é uma coluna que guarda o valor da chave
primária de outra tabela, e é ela que permite juntar as duas em uma consulta só.
O par de sintaxe que define o relacionamento
-- chave primaria de uma tabela <coluna> <tipo> AUTO_INCREMENT PRIMARY KEY -- chave estrangeira, com o nome dado em CONSTRAINT CONSTRAINT <nome> FOREIGN KEY (<coluna>) REFERENCES <tabela> (<chave>)
Um aluno se matricula em vários cursos e um curso tem vários alunos: esse
relacionamento é N:N e ninguém consegue guardá-lo em uma tabela só. Quem resolve
é uma tabela de junção, no modelo matriculas, que guarda o par aluno_id e
curso_id — uma linha por matrícula.
Exemplo
<?php declare(strict_types=1); // Modelo do curso: um banco, tres tabelas e os relacionamentos. // Nao ha conexao com servidor nenhum: o script imprime o SQL que cria a estrutura. $sql = <<<'SQL' -- o banco guarda o conjunto inteiro do curso CREATE DATABASE IF NOT EXISTS escola CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE escola; -- tabela: linha e curso, coluna e atributo do curso CREATE TABLE IF NOT EXISTS cursos ( id INT AUTO_INCREMENT PRIMARY KEY, nome VARCHAR(120) NOT NULL, carga_horaria INT NOT NULL DEFAULT 40, valor DECIMAL(10,2) NOT NULL, ativo TINYINT(1) NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS alunos ( id INT AUTO_INCREMENT PRIMARY KEY, nome VARCHAR(120) NOT NULL, email VARCHAR(180) NOT NULL, cidade VARCHAR(80), nascimento DATE, criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- tabela de juncao: e ela que resolve o relacionamento N:N entre as duas CREATE TABLE IF NOT EXISTS matriculas ( id INT AUTO_INCREMENT PRIMARY KEY, aluno_id INT NOT NULL, curso_id INT NOT NULL, data_matricula DATE NOT NULL, situacao ENUM('ativa', 'cancelada', 'concluida') NOT NULL DEFAULT 'ativa', CONSTRAINT fk_matricula_aluno FOREIGN KEY (aluno_id) REFERENCES alunos (id), CONSTRAINT fk_matricula_curso FOREIGN KEY (curso_id) REFERENCES cursos (id) ); SQL; echo $sql . "\n";
Saída real
-- o banco guarda o conjunto inteiro do curso
CREATE DATABASE IF NOT EXISTS escola
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE escola;
-- tabela: linha e curso, coluna e atributo do curso
CREATE TABLE IF NOT EXISTS cursos (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
carga_horaria INT NOT NULL DEFAULT 40,
valor DECIMAL(10,2) NOT NULL,
ativo TINYINT(1) NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS alunos (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
email VARCHAR(180) NOT NULL,
cidade VARCHAR(80),
nascimento DATE,
criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- tabela de juncao: e ela que resolve o relacionamento N:N entre as duas
CREATE TABLE IF NOT EXISTS matriculas (
id INT AUTO_INCREMENT PRIMARY KEY,
aluno_id INT NOT NULL,
curso_id INT NOT NULL,
data_matricula DATE NOT NULL,
situacao ENUM('ativa', 'cancelada', 'concluida') NOT NULL DEFAULT 'ativa',
CONSTRAINT fk_matricula_aluno FOREIGN KEY (aluno_id) REFERENCES alunos (id),
CONSTRAINT fk_matricula_curso FOREIGN KEY (curso_id) REFERENCES cursos (id)
);
Tipos de dados no SQL
Tipos numéricos
O tipo da coluna decide o que o banco guarda, quanto espaço ocupa e como compara.
Em SQL, 1234 e '1234' não são o mesmo valor: o primeiro é inteiro, o segundo
é texto. DECIMAL é o tipo obrigatório para dinheiro, porque FLOAT e DOUBLE
guardam o número em notação científica e 0.1 + 0.2 não dá exatamente 0.3.
Tipos numéricos e o que cada um guarda
| Tipo | Tamanho | Quando usar |
|---|---|---|
TINYINT | 1 byte | 0 e 1, usado como booleano |
SMALLINT | 2 bytes | Contagem pequena, número de vagas |
INT | 4 bytes | Identificador, quantidade, ano |
BIGINT | 8 bytes | Identificador acima de 2 bilhões de linhas |
DECIMAL(M,D) | exato | Dinheiro; M dígitos totais, D decimais |
FLOAT | 4 bytes, aproximado | Cálculo científico |
DOUBLE | 8 bytes, aproximado | Cálculo científico de precisão maior |
BIT(1) | 1 bit | Verdadeiro e falso |
Tipos de texto
CHAR(n) ocupa sempre n bytes e serve para código de tamanho fixo; VARCHAR(n)
ocupa só o que foi gravado, até o limite de n caracteres. ENUM limita a coluna
a uma lista de valores e tem um preço: o conjunto fica gravado na própria tabela,
e incluir um valor novo exige ALTER TABLE.
Tipos de texto e limite de tamanho
| Tipo | Limite | Quando usar |
|---|---|---|
CHAR(n) | n bytes, tamanho fixo | Sigla, código de 2 letras |
VARCHAR(n) | n caracteres | Nome, e-mail, cidade |
TINYTEXT | 255 bytes | Observação curta |
TEXT | 65.535 bytes | Texto longo sem formatação |
MEDIUMTEXT | 16 MB | Descrição grande |
LONGTEXT | 4 GB | Documento inteiro |
ENUM('a','b') | lista de valores | situacao da matrícula |
JSON | documento com chave e valor | Configuração que muda de forma |
Tipos de data, hora e o fim da definição de coluna
DATE guarda só a data e basta para nascimento e matrícula. DATETIME grava o
que foi digitado, sem conversão; TIMESTAMP grava um número inteiro de
segundos, ocupa menos espaço, mas fica em UTC e tem intervalo curto, de 1970 a
2038.
A forma de fechar a definição de uma coluna
<coluna> <tipo> [AUTO_INCREMENT] [NOT NULL] [DEFAULT <valor>] ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = <collation>;
AUTO_INCREMENT só entra em coluna inteira que seja chave primária. O conjunto
de caracteres vem no final da tabela e vale para todas as colunas de texto:
utf8mb4 é a única opção que guarda qualquer caractere Unicode, e o utf8 do
próprio MySQL é incompleto e quebra em texto com emoji.
Exemplo
<?php declare(strict_types=1); // Os tipos de coluna na pratica: uma tabela que usa quase todos. // Vale para ler a coluna e saber o que o banco vai aceitar nela. $sql = <<<'SQL' CREATE TABLE IF NOT EXISTS professores ( -- id numerico gerado pelo proprio banco id INT AUTO_INCREMENT PRIMARY KEY, -- nome variavel: o maior tamanho que ja foi gravado e o limite nome VARCHAR(120) NOT NULL, -- sigla tem tamanho fixo: ocupa sempre os 4 bytes sigla CHAR(4) NOT NULL, -- email nao se repete: e o que o login usa email VARCHAR(180) NOT NULL, -- dinheiro em exato, nunca FLOAT salario DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- contagem pequena cabe em 2 bytes horas_semana SMALLINT UNSIGNED NOT NULL DEFAULT 40, -- booleano e TINYINT(1) gravado como 0 ou 1 ativo TINYINT(1) NOT NULL DEFAULT 1, -- lista controlada area VARCHAR(60) NOT NULL, formacao ENUM('licenciatura', 'pos', 'tecnologo') NOT NULL, -- data sem hora e suficiente admissao DATE NOT NULL, -- data e hora sem fuso: o que entra e o que sai atualizado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- documento livre, util para configuracao que muda de forma preferencias JSON NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- forma de gravar e ler os tipos no dia a dia INSERT INTO professores (nome, sigla, email, salario, horas_semana, area, formacao, admissao, preferencias) VALUES ('Helena Prado', 'HP', '[email protected]', 7200.00, 40, 'Banco de dados', 'pos', '2019-03-04', '{"turno": "noite", "aulas_online": true}'), ('Igor Salles', 'IS', '[email protected]', 6100.50, 30, 'Programacao', 'licenciatura', '2021-08-16', NULL); SQL; echo $sql . "\n";
Saída real
CREATE TABLE IF NOT EXISTS professores (
-- id numerico gerado pelo proprio banco
id INT AUTO_INCREMENT PRIMARY KEY,
-- nome variavel: o maior tamanho que ja foi gravado e o limite
nome VARCHAR(120) NOT NULL,
-- sigla tem tamanho fixo: ocupa sempre os 4 bytes
sigla CHAR(4) NOT NULL,
-- email nao se repete: e o que o login usa
email VARCHAR(180) NOT NULL,
-- dinheiro em exato, nunca FLOAT
salario DECIMAL(10,2) NOT NULL DEFAULT 0.00,
-- contagem pequena cabe em 2 bytes
horas_semana SMALLINT UNSIGNED NOT NULL DEFAULT 40,
-- booleano e TINYINT(1) gravado como 0 ou 1
ativo TINYINT(1) NOT NULL DEFAULT 1,
-- lista controlada
area VARCHAR(60) NOT NULL,
formacao ENUM('licenciatura', 'pos', 'tecnologo') NOT NULL,
-- data sem hora e suficiente
admissao DATE NOT NULL,
-- data e hora sem fuso: o que entra e o que sai
atualizado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- documento livre, util para configuracao que muda de forma
preferencias JSON NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-- forma de gravar e ler os tipos no dia a dia
INSERT INTO professores (nome, sigla, email, salario, horas_semana, area, formacao, admissao, preferencias)
VALUES ('Helena Prado', 'HP', '[email protected]', 7200.00, 40, 'Banco de dados', 'pos', '2019-03-04', '{"turno": "noite", "aulas_online": true}'),
('Igor Salles', 'IS', '[email protected]', 6100.50, 30, 'Programacao', 'licenciatura', '2021-08-16', NULL);
CREATE TABLE e constraints
A sintaxe do CREATE TABLE
CREATE TABLE descreve a estrutura: nome da tabela, nome e tipo de cada coluna e
as regras que valem para o conjunto. Entre parênteses, uma coluna por linha,
separadas por vírgula, e a vírgula sobrando é erro de sintaxe.
A forma geral, com regra junto da coluna e regra no fim
CREATE TABLE [IF NOT EXISTS] <tabela> ( <coluna> <tipo> [PRIMARY KEY] [NOT NULL] [UNIQUE] [DEFAULT <valor>], ... [CONSTRAINT <nome> UNIQUE (<colunas>)] ) ENGINE = InnoDB;
As regras que valem linha a linha
NOT NULL proíbe o vazio, UNIQUE proíbe valor repetido e DEFAULT define o
que entra quando a coluna é omitida. UNSIGNED tira os negativos da faixa, e
ZEROFILL ainda completa com zeros à esquerda — raramente é o que se quer num
identificador.
Restrições mais usadas
| Restrição | O que impede |
|---|---|
NOT NULL | Gravar NULL na coluna |
UNIQUE | Repetir o valor em outra linha |
DEFAULT valor | Gravar a coluna vazia sem valor explícito |
PRIMARY KEY (col) | Linha sem identificação e valor repetido |
AUTO_INCREMENT | O valor da chave ser digitado à mão |
UNSIGNED | Números negativos |
CHECK (condicao) | Valor que não satisfaz a condição |
ON UPDATE CURRENT_TIMESTAMP | Data desatualizada a cada UPDATE |
Chave estrangeira e o que acontece ao apagar
A chave estrangeira precisa do ON DELETE e do ON UPDATE definidos, senão o
MySQL recusa a tabela. O que está entre parênteses é a reação do banco quando
alguém apaga a linha na tabela referenciada — e escolher CASCADE em
matriculas significa que apagar o aluno apaga as matrículas dele junto.
O par que não pode faltar
CONSTRAINT <nome> FOREIGN KEY (<coluna>) REFERENCES <tabela> (<chave>) ON DELETE <acao> ON UPDATE <acao>
| Ação | Efeito |
|---|---|
RESTRICT | Bloqueia o apagamento enquanto existir referência |
CASCADE | Apaga ou altera a linha filha junto |
SET NULL | Zera a coluna filha; exige NULL permitido nela |
NO ACTION | Igual a RESTRICT, verificada no fim da instrução |
O motor tem de ser InnoDB para tudo isso funcionar: o MyISAM ignora chave
estrangeira e não tem transação.
Exemplo
<?php declare(strict_types=1); // CREATE TABLE com todas as restricoes que o modelo usa. // ON DELETE e ON UPDATE nao sao opcionais: sem eles o MySQL recusa a tabela. $sql = <<<'SQL' CREATE TABLE IF NOT EXISTS matriculas ( -- chave primaria simples, gerada pelo banco id INT AUTO_INCREMENT, -- chave estrangeira para os dois lados do relacionamento aluno_id INT NOT NULL, curso_id INT NOT NULL, -- mesma matricula nao pode existir duas vezes data_matricula DATE NOT NULL, situacao ENUM('ativa', 'cancelada', 'concluida') NOT NULL DEFAULT 'ativa', -- valor tem que estar entre 0 e 100 nota_final DECIMAL(5,2) NULL, -- data da ultima mudanca, mexida sozinha a cada UPDATE atualizado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY ux_matricula (aluno_id, curso_id), CONSTRAINT ck_nota CHECK (nota_final IS NULL OR (nota_final >= 0 AND nota_final <= 100)), -- apagar o aluno leva as matriculas dele junto CONSTRAINT fk_matricula_aluno FOREIGN KEY (aluno_id) REFERENCES alunos (id) ON DELETE CASCADE ON UPDATE CASCADE, -- apagar o curso bloqueia enquanto houver matricula CONSTRAINT fk_matricula_curso FOREIGN KEY (curso_id) REFERENCES cursos (id) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- conferir o que o banco gravou, antes de confiar no papel DESCRIBE matriculas; SQL; echo $sql . "\n";
Saída real
CREATE TABLE IF NOT EXISTS matriculas (
-- chave primaria simples, gerada pelo banco
id INT AUTO_INCREMENT,
-- chave estrangeira para os dois lados do relacionamento
aluno_id INT NOT NULL,
curso_id INT NOT NULL,
-- mesma matricula nao pode existir duas vezes
data_matricula DATE NOT NULL,
situacao ENUM('ativa', 'cancelada', 'concluida') NOT NULL DEFAULT 'ativa',
-- valor tem que estar entre 0 e 100
nota_final DECIMAL(5,2) NULL,
-- data da ultima mudanca, mexida sozinha a cada UPDATE
atualizado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY ux_matricula (aluno_id, curso_id),
CONSTRAINT ck_nota CHECK (nota_final IS NULL OR (nota_final >= 0 AND nota_final <= 100)),
-- apagar o aluno leva as matriculas dele junto
CONSTRAINT fk_matricula_aluno FOREIGN KEY (aluno_id) REFERENCES alunos (id)
ON DELETE CASCADE ON UPDATE CASCADE,
-- apagar o curso bloqueia enquanto houver matricula
CONSTRAINT fk_matricula_curso FOREIGN KEY (curso_id) REFERENCES cursos (id)
ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-- conferir o que o banco gravou, antes de confiar no papel
DESCRIBE matriculas;
Índices e normalização
Índice
Índice é uma estrutura auxiliar que o banco mantém ordenada para achar a linha
mais rápido, do mesmo jeito que o índice remissivo de um livro. Ele não
organiza o resultado da consulta — isso é ORDER BY — e sim economiza tempo de
procura. O custo é espaço em disco e mais trabalho a cada INSERT, UPDATE e
DELETE.
Os dois tipos de índice
CREATE UNIQUE INDEX ix_alunos_email ON alunos (email); CREATE INDEX ix_matriculas_curso ON matriculas (curso_id); DROP INDEX ix_alunos_email ON alunos;
| Tipo | O que faz |
|---|---|
PRIMARY KEY | Índice único e obrigatório, criado junto com a tabela |
UNIQUE | Proíbe valor repetido, como o UNIQUE da coluna |
INDEX | Índice comum, acelera a busca e não impede repetição |
FULLTEXT | Busca por palavra em colunas de texto |
COMPOSITE | Índice de duas ou mais colunas juntas |
Onde o índice ajuda e onde atrapalha
O índice é usado quando a coluna está no WHERE, no JOIN, no ORDER BY ou no
GROUP BY. EXPLAIN mostra se ele foi usado: a coluna key da saída traz o nome
do índice, e type: ALL significa varredura completa, que é o sinal de que a
tabela precisa de índice.
Leitura do EXPLAIN
EXPLAIN SELECT * FROM matriculas WHERE curso_id = 3;
| Valor | Significado |
|---|---|
type: const | Busca por chave primária, o melhor caso |
type: ref | Usa índice não único, poucas linhas lidas |
type: ALL | Varredura de toda a tabela |
rows | Estimativa de linhas examinadas |
Extra: Using filesort | Ordenação feita em memória, sem índice |
Índice em coluna que quase todo mundo deixa nula só ocupa espaço. Índice
composto é usado só pelo lado esquerdo: (curso_id, situacao) não serve para
filtrar só por situacao, e a ordem das colunas importa.
Normalização
Normalizar é dividir a tabela para que cada dado fique guardado uma vez só, em uma
tabela que o descreve. Sem isso o mesmo nome do curso se repete em
cada linha de matriculas, e corrigir o nome exige acertar todas de uma vez.
As três formas normais, resumidas
| Forma | Regra | O que aparece na prática |
|---|---|---|
| 1FN | Um valor por célula, sem lista nem grupo | Endereço em colunas separadas |
| 2FN | Nenhuma coluna depende só de parte da chave | Cidade e estado saem para cidades |
| 3FN | Nenhuma coluna depende de outra coluna que não seja chave | nome_curso sai de matriculas |
| Termo | Significado |
|---|---|
| Redundância | Dado repetido em várias linhas |
| Anomalia de inserção | Não dá para gravar um fato sem gravar outro |
| Anomalia de atualização | Corrigir em um lugar e esquecer o outro |
| Chave primária composta | Chave formada por duas ou mais colunas |
Nem toda base precisa chegar à 3FN. Tabela de log ou de relatório é lida com
freqência e desnormalizar nela é uma decisão válida, desde que consciente.
Exemplo
<?php declare(strict_types=1); // Indices e normalizacao: o que o banco faz sozinho e o que a modelagem resolve. // EXPLAIN aparece aqui como texto; a execucao dele depende de um banco com dados. $sql = <<<'SQL' -- 1) indice unico: dois alunos nao podem ter o mesmo email CREATE UNIQUE INDEX ux_alunos_email ON alunos (email); -- 2) indice composto: acelera filtrar por curso e sobe a situacao CREATE INDEX ix_matriculas_curso_situacao ON matriculas (curso_id, situacao); -- 3) 2FN: cidade e estado dependem da cidade, nao da matricula. -- Nao sao dados do aluno: sao dados do lugar onde ele mora. CREATE TABLE IF NOT EXISTS cidades ( id INT AUTO_INCREMENT PRIMARY KEY, nome VARCHAR(80) NOT NULL, estado CHAR(2) NOT NULL, UNIQUE KEY ux_cidade_estado (nome, estado) ); -- 4) 3FN: o nome do curso nao e dado da matricula, e dado do curso. -- Depois desta separacao, renomear o curso e um UPDATE so. ALTER TABLE matriculas DROP COLUMN nome_curso; -- como se descobre qual indice a consulta esta usando EXPLAIN SELECT id, aluno_id FROM matriculas WHERE curso_id = 3; EXPLAIN SELECT id FROM alunos WHERE email = '[email protected]'; SQL; echo $sql . "\n";
Saída real
-- 1) indice unico: dois alunos nao podem ter o mesmo email CREATE UNIQUE INDEX ux_alunos_email ON alunos (email); -- 2) indice composto: acelera filtrar por curso e sobe a situacao CREATE INDEX ix_matriculas_curso_situacao ON matriculas (curso_id, situacao); -- 3) 2FN: cidade e estado dependem da cidade, nao da matricula. -- Nao sao dados do aluno: sao dados do lugar onde ele mora. CREATE TABLE IF NOT EXISTS cidades ( id INT AUTO_INCREMENT PRIMARY KEY, nome VARCHAR(80) NOT NULL, estado CHAR(2) NOT NULL, UNIQUE KEY ux_cidade_estado (nome, estado) ); -- 4) 3FN: o nome do curso nao e dado da matricula, e dado do curso. -- Depois desta separacao, renomear o curso e um UPDATE so. ALTER TABLE matriculas DROP COLUMN nome_curso; -- como se descobre qual indice a consulta esta usando EXPLAIN SELECT id, aluno_id FROM matriculas WHERE curso_id = 3; EXPLAIN SELECT id FROM alunos WHERE email = '[email protected]';