Dia 11 — JOIN
Chave estrangeira e relacionamentos
Chave estrangeira e relacionamentos
A chave estrangeira é a coluna que diz esta linha depende daquela. É ela que
impede o dado órfão: um pedido apontando para um cliente que não existe mais, um
aluno de uma turma que foi apagada.
CREATE TABLE tb_aluno ( id INT AUTO_INCREMENT PRIMARY KEY, nm_aluno VARCHAR(40) NOT NULL, id_curso INT NOT NULL, CONSTRAINT fk_aluno_curso FOREIGN KEY (id_curso) REFERENCES tb_curso (id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB;
A ordem de criação importa: a tabela com a chave estrangeira vem depois da que
ela referencia. O banco recusa com ER_CANT_CREATE_FOREIGN_KEY se o pai ainda
não existir. E a FK só existe em ENGINE=InnoDB — o ENGINE é parte da
declaração, não um detalhe.
O nome da tabela é tb_d<dia>a<n>_<assunto>
Cada exemplo do material usa tabela própria, com o dia e a aula no nome. Não é
enfeite: dois exemplos que usem tb_aluno criam a mesma tabela, e a chave
estrangeira de um passa a bloquear o TRUNCATE do outro com
ER_TRUNCATE_ILLEGAL_FK — o erro aparece em um arquivo que não tem nada a ver
com chave estrangeira, e a causa real está a quinze linhas de distância.
| Padrão | Exemplo |
|---|---|
tb_d<dia>a<numero>_<assunto> | tb_d11a1_aluno, tb_d14a2_transacao |
É a mesma exigência de IF NOT EXISTS + TRUNCATE da aula 8, aplicada um nível
acima: o exemplo não pode depender do estado em que outro exemplo deixou o banco.
Os três relacionamentos
| Tipo | Desenho | Onde fica a FK |
|---|---|---|
| um para muitos | 1 curso → N alunos | tb_aluno.id_curso |
| muitos para um | N pedidos → 1 cliente | o mesmo desenho, visto do outro lado |
| muitos para muitos | N alunos ↔ N cursos | tabela associativa |
O muitos para muitos não cabe em duas tabelas: nem tb_aluno nem tb_curso
aguenta duas chaves estrangeiras ao mesmo tempo. A terceira tabela guarda os dois
lados, e a PRIMARY KEY composta é o que impede a inscrição repetida:
CREATE TABLE tb_matricula ( id_aluno INT NOT NULL, id_curso INT NOT NULL, PRIMARY KEY (id_aluno, id_curso), -- composta: impede a repetida CONSTRAINT fk_matricula_aluno FOREIGN KEY (id_aluno) REFERENCES tb_aluno (id), CONSTRAINT fk_matricula_curso FOREIGN KEY (id_curso) REFERENCES tb_curso (id) );
A inscrição repetida dá ER_DUP_ENTRY, e é esse code que o Node compara.
ON DELETE: as três regras
| Regra | Apagar o pai faz |
|---|---|
CASCADE | apaga os filhos também |
SET NULL | zera a coluna do filho — exige coluna NULL |
RESTRICT | recusa enquanto houver filho (o padrão) |
NO ACTION | igual ao RESTRICT na maioria dos casos |
CASCADE é a mais perigosa das três, e a única que um exemplo executa — porque ali
o pai é dado de teste. SET NULL exige coluna nullable: uma coluna NOT NULL não
tem como receber o NULL que a regra pede, e o ALTER que declara a regra falha
nesse caso.
ON UPDATE CASCADE propaga a troca de id do pai para o filho. Sem ele, o UPDATE
do id do pai é recusado enquanto houver filho apontando.
Integridade referencial: o banco recusando
A FK transforma "dependência quebrada" em erro, e não em dado inválido. As três
violações que importam, cada uma com code próprio:
| Violação | erro.code |
|---|---|
| filho apontando para pai inexistente | ER_NO_REFERENCED_ROW_2 |
| dois registros com o mesmo id de pai | ER_DUP_ENTRY |
apagar o pai com filho, sob RESTRICT | ER_ROW_IS_REFERENCED_2 |
O code é o que o Node compara. A mensagem muda entre versões do servidor, e
comparar mensagem é comparar string que muda.
A FK cria o índice
Toda chave estrangeira cria um índice, e é isso que faz o JOIN daquela tabela
ser rápido. SHOW INDEX FROM t mostra o Key_name da FK — e é por isso que
id_curso não precisa de INDEX separado.
Normalização, em uma frase
Guardar nm_curso dentro de tb_aluno repetiria o mesmo texto em toda linha:
mudar o nome do curso exigiria um UPDATE em N linhas, e duas linhas poderiam
ficar com nomes diferentes. A FK guarda o id, e o nome fica em um lugar só —
a terceira forma normal.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 11: chave estrangeira e os tres relacionamentos. // // A chave estrangeira e a coluna que diz que esta linha depende daquela. E ela // que impede o dado orfao: um pedido que aponta para um cliente que nao existe // mais, ou um aluno de uma turma que foi apagada. // // O exemplo cria as duas tabelas com `ON DELETE CASCADE` e `ON DELETE SET NULL`, // e depois mede os tres relacionamentos: um para muitos, muitos para um e muitos // para muitos com tabela associativa. E mede o que o banco FAZ quando a regra e // violada, que e a parte que so aparece em execucao. const mysql = require('mysql2/promise'); async function main() { const conexao = await mysql.createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, }); try { // // A ordem de criacao importa: a tabela que tem a chave estrangeira vem DEPOIS // da que ela referencia. O banco recusa com `ER_CANT_CREATE_FOREIGN_KEY` se o // `tb_d11_curso` ainda nao existir. // // E o prefixo `tb_d11a1_` no nome e regra do material, nao enfeite: cada // exemplo usa tabela PROPRIA. Sem o prefixo, este exemplo usaria `tb_aluno` e // `tb_curso`, que sao os nomes da aula 2 do dia 8 — e a chave estrangeira // criada aqui passaria a bloquear o `TRUNCATE` daquele exemplo, com // `ER_TRUNCATE_ILLEGAL_FK`, num arquivo que nao tem nada a ver com chave // estrangeira. Medido: foi exatamente isso que aconteceu na primeira versao // deste exemplo, e o sintoma apareceu em outro dia do material. await conexao.query('DROP TABLE IF EXISTS tb_d11_matricula'); await conexao.query('DROP TABLE IF EXISTS tb_d11_aluno'); await conexao.query('DROP TABLE IF EXISTS tb_turma'); await conexao.query('DROP TABLE IF EXISTS tb_d11_curso'); // --- 1. a tabela do lado "um" --- await conexao.query(` CREATE TABLE tb_d11_curso ( id INT AUTO_INCREMENT PRIMARY KEY, nm_curso VARCHAR(40) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await conexao.query( 'INSERT INTO tb_d11_curso (nm_curso) VALUES (?), (?), (?)', ['Node.js', 'MySQL', 'HTTP'] ); // --- 2. a tabela do lado "muitos", com a chave estrangeira --- // // `ON DELETE CASCADE`: apagar o curso apaga os alunos dele. E a regra mais // perigosa das tres, e e a unica que o material usa em exemplo — porque // aqui o curso e dado de teste, e o aluno nao tem nada a perder. await conexao.query(` CREATE TABLE tb_d11_aluno ( id INT AUTO_INCREMENT PRIMARY KEY, nm_aluno VARCHAR(40) NOT NULL, id_curso INT NOT NULL, CONSTRAINT fk_d11_aluno_curso FOREIGN KEY (id_curso) REFERENCES tb_d11_curso (id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await conexao.query( 'INSERT INTO tb_d11_aluno (nm_aluno, id_curso) VALUES (?, ?), (?, ?), (?, ?), (?, ?)', ['Ana', 1, 'Bruno', 1, 'Carla', 2, 'Diego', 3] ); console.log('--- 3 cursos e 4 alunos ---'); const [cursos] = await conexao.query('SELECT id, nm_curso FROM tb_d11_curso ORDER BY id'); const [alunos] = await conexao.query('SELECT id, nm_aluno, id_curso FROM tb_d11_aluno ORDER BY id'); for (const c of cursos) console.log(' curso ' + c.id + ': ' + c.nm_curso); for (const a of alunos) console.log(' aluno ' + a.id + ': ' + a.nm_aluno + ' -> curso ' + a.id_curso); // --- 3. os tres relacionamentos --- console.log(''); console.log('--- os tres relacionamentos ---'); console.log('um para muitos: 1 curso -> N alunos. tb_d11_aluno.id_curso aponta para tb_d11_curso.id'); const [umParaMuitos] = await conexao.query( 'SELECT c.nm_curso, COUNT(a.id) AS alunos FROM tb_d11_curso c ' + 'LEFT JOIN tb_d11_aluno a ON a.id_curso = c.id GROUP BY c.id, c.nm_curso ORDER BY c.id' ); for (const l of umParaMuitos) console.log(' ' + l.nm_curso.padEnd(10) + ' -> ' + l.alunos + ' aluno(s)'); console.log(' (o `LEFT JOIN` mantem o curso que nao tem aluno nenhum — o `GROUP BY` conta)'); console.log('muitos para um: N alunos -> 1 curso. E o mesmo desenho, visto do outro lado'); console.log(' `id_curso` em tb_d11_aluno e a chave estrangeira; `id` em tb_d11_curso e a chave primaria.'); console.log('muitos para muitos: N alunos <-> N cursos, e precisa de uma TERCEIRA tabela.'); console.log(' Nem tb_d11_aluno nem tb_d11_curso aguenta duas chaves estrangeiras ao mesmo tempo,'); console.log(' entao a tabela associativa guarda os dois lados:'); await conexao.query(` CREATE TABLE tb_d11_matricula ( id_aluno INT NOT NULL, id_curso INT NOT NULL, dt_inscricao DATE NOT NULL, PRIMARY KEY (id_aluno, id_curso), CONSTRAINT fk_d11_matricula_aluno FOREIGN KEY (id_aluno) REFERENCES tb_d11_aluno (id) ON DELETE CASCADE, CONSTRAINT fk_d11_matricula_curso FOREIGN KEY (id_curso) REFERENCES tb_d11_curso (id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await conexao.query( 'INSERT INTO tb_d11_matricula (id_aluno, id_curso, dt_inscricao) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)', [1, 1, '2026-05-04', 1, 2, '2026-05-04', 2, 1, '2026-05-11'] ); // // A data vem por `DATE_FORMAT`, e nao por `toISOString()`: o `date` lido como // `Date` do JavaScript entra com fuso e sai no dia anterior no `toISOString()`. const [matriculas] = await conexao.query( "SELECT a.nm_aluno, c.nm_curso, DATE_FORMAT(m.dt_inscricao, '%Y-%m-%d') AS dt_inscricao " + 'FROM tb_d11_matricula m ' + 'JOIN tb_d11_aluno a ON a.id = m.id_aluno JOIN tb_d11_curso c ON c.id = m.id_curso ORDER BY a.nm_aluno, c.nm_curso' ); console.log(' as inscricoes:'); for (const m of matriculas) { console.log(' ' + m.nm_aluno.padEnd(7) + ' -> ' + m.nm_curso + ' em ' + m.dt_inscricao); } console.log(' `Ana` esta em dois cursos: e a assinatura de um muitos para muitos.'); console.log(' A `PRIMARY KEY (id_aluno, id_curso)` composta impede a inscricao repetida:'); try { await conexao.query( 'INSERT INTO tb_d11_matricula (id_aluno, id_curso, dt_inscricao) VALUES (?, ?, ?)', [1, 1, '2026-05-18'] ); } catch (erro) { console.error(erro.code + ': ' + erro.message); console.log(' erro esperado:', erro.code, '- a mesma inscricao ja existe'); } // --- 4. `ON DELETE`: as tres regras --- console.log(''); console.log('--- ON DELETE: as tres regras ---'); console.log('CASCADE | apagar o pai apaga os filhos'); console.log('SET NULL | apagar o pai zera a coluna do filho (exige coluna NULL)'); console.log('RESTRICT | apagar o pai e RECUSADO enquanto houver filho (o padrao)'); console.log('NO ACTION | igual ao RESTRICT na maioria dos casos'); await conexao.query('ALTER TABLE tb_d11_aluno MODIFY id_curso INT NULL'); await conexao.query( 'ALTER TABLE tb_d11_aluno DROP FOREIGN KEY fk_d11_aluno_curso' ); await conexao.query( 'ALTER TABLE tb_d11_aluno ADD CONSTRAINT fk_d11_aluno_curso FOREIGN KEY (id_curso) REFERENCES tb_d11_curso (id) ON DELETE SET NULL' ); const [antesDoSetNull] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d11_aluno WHERE id_curso = 3'); await conexao.query('DELETE FROM tb_d11_curso WHERE id = ?', [3]); const [depoisDoSetNull] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d11_aluno WHERE id_curso = 3'); const [orfao] = await conexao.query('SELECT nm_aluno, id_curso FROM tb_d11_aluno WHERE id = ?', [4]); console.log(''); console.log('--- medindo o SET NULL ---'); console.log('alunos do curso 3 antes:', antesDoSetNull[0].n); await conexao.query('DELETE FROM tb_d11_curso WHERE id = ?', [3]); console.log('alunos do curso 3 depois:', depoisDoSetNull[0].n); console.log('o aluno que era do curso 3:', JSON.stringify(orfao[0])); console.log(' o curso sumiu, o aluno ficou, e o id_curso dele virou NULL.'); console.log(' e por isso que `SET NULL` exige coluna NULL: uma coluna NOT NULL nao'); console.log(' tem como receber o NULL que a regra pede.'); // --- 5. `ON UPDATE CASCADE` --- // // Alterar o id do pai propaga para o filho. Sem `CASCADE`, o `UPDATE` do id // pai e recusado com `ER_ROW_IS_REFERENCED_2` enquanto houver filho apontando. // // O curso usado aqui e o 2, e nao o 1, por um motivo concreto: a tabela // associativa tb_d11_matricula tambem referencia tb_d11_curso com CASCADE, e tanto o // curso 1 quanto o 2 tem matricula. Trocar o id quebraria a FK da matricula // antes de chegar na FK do aluno, e o erro seria de outra tabela — o que // ensinaria a coisa errada. A medicao tem de acontecer na FK que ela quer. // // Por isso as matriculas sao limpas antes: a secao mede a regra de // ON UPDATE, e nao a de ON DELETE. Depois desta secao, a tabela associativa // ja cumpriu o papel dela, que e a aula 1 mostrar o desenho. await conexao.query('TRUNCATE TABLE tb_d11_matricula'); console.log(''); console.log('--- ON UPDATE CASCADE ---'); await conexao.query('ALTER TABLE tb_d11_aluno DROP FOREIGN KEY fk_d11_aluno_curso'); await conexao.query( 'ALTER TABLE tb_d11_aluno ADD CONSTRAINT fk_d11_aluno_curso FOREIGN KEY (id_curso) REFERENCES tb_d11_curso (id) ON DELETE SET NULL ON UPDATE CASCADE' ); const [alunosAntes] = await conexao.query('SELECT id, nm_aluno, id_curso FROM tb_d11_aluno ORDER BY id'); await conexao.query('UPDATE tb_d11_curso SET id = ? WHERE id = ?', [50, 2]); const [alunosDepois] = await conexao.query('SELECT id, nm_aluno, id_curso FROM tb_d11_aluno ORDER BY id'); console.log(' tb_d11_curso.id 2 virou 50; os alunos que apontavam para 2:'); console.log(' antes: ' + (alunosAntes.filter((a) => a.id_curso === 2).map((a) => a.nm_aluno + '=' + a.id_curso).join(', ') || 'ninguem')); console.log(' depois: ' + (alunosDepois.filter((a) => a.id_curso === 50).map((a) => a.nm_aluno + '=' + a.id_curso).join(', ') || 'ninguem')); console.log(' o CASCADE atualizou o filho sozinho. Sem ele, o UPDATE seria recusado.'); // --- 6. integridade referencial: o banco recusando --- // // A FK existe para transformar "dependencia quebrada" em erro, e nao em dado // invalido. As tres violacoes que importam, cada uma com um `code` proprio. console.log(''); console.log('--- integridade referencial: o banco recusando ---'); // (a) filho apontando para pai que nao existe try { await conexao.query('INSERT INTO tb_d11_aluno (nm_aluno, id_curso) VALUES (?, ?)', ['Orfao', 999]); console.log(' (a) filho apontando para pai inexistente -> ACEITOU'); } catch (erro) { console.error(' ' + erro.code + ': ' + erro.message); console.log(' (a) filho apontando para pai inexistente -> recusado, code', erro.code); } // (b) `ON UPDATE CASCADE` levando um pai com filho para um id que ja existe // como chave de outro pai: isso faria dois cursos com o mesmo id. await conexao.query('ALTER TABLE tb_d11_aluno DROP FOREIGN KEY fk_d11_aluno_curso'); await conexao.query( 'ALTER TABLE tb_d11_aluno ADD CONSTRAINT fk_d11_aluno_curso FOREIGN KEY (id_curso) REFERENCES tb_d11_curso (id) ON DELETE SET NULL ON UPDATE CASCADE' ); await conexao.query('UPDATE tb_d11_curso SET id = ? WHERE id = ?', [60, 50]); await conexao.query('INSERT INTO tb_d11_curso (id, nm_curso) VALUES (?, ?)', [70, 'Reuniao']); try { // O curso 60 tem a Carla apontando. Levar o 60 para 70 faz a Carla apontar // para o mesmo id que o curso 60 — e o banco recusa. await conexao.query('UPDATE tb_d11_curso SET id = ? WHERE id = ?', [70, 60]); console.log(' (b) dois cursos com o mesmo id -> ACEITOU'); } catch (erro) { console.error(' ' + erro.code + ': ' + erro.message); console.log(' (b) dois cursos com o mesmo id -> recusado, code', erro.code); } console.log(' o `ON UPDATE CASCADE` propagou o id novo para a Carla e, em seguida, o'); console.log(' banco viu que dois cursos teriam o mesmo id, e recusou o segundo.'); // (c) apagar o pai com filho, sob RESTRICT await conexao.query('ALTER TABLE tb_d11_aluno DROP FOREIGN KEY fk_d11_aluno_curso'); await conexao.query( 'ALTER TABLE tb_d11_aluno ADD CONSTRAINT fk_d11_aluno_curso FOREIGN KEY (id_curso) REFERENCES tb_d11_curso (id) ON DELETE RESTRICT' ); try { await conexao.query('DELETE FROM tb_d11_curso WHERE id = ?', [60]); console.log(' (c) apagar o pai com filho, sob RESTRICT -> ACEITOU'); } catch (erro) { console.error(' ' + erro.code + ': ' + erro.message); console.log(' (c) apagar o pai com filho, sob RESTRICT -> recusado, code', erro.code); } console.log(' o mesmo DELETE sob CASCADE teria apagado o filho tambem, sem erro.'); console.log(''); console.log('os tres codes sao diferentes, e e por isso que o `erro.code` e o que o'); console.log('Node compara: a mensagem muda entre versoes do servidor.'); // --- 7. a chave estrangeira como indice --- // // Toda chave estrangeira cria um indice. E o que faz o `JOIN` daquela tabela // ser rapido, e o motivo de `id_curso` nao precisar de `INDEX` separado. const [indices] = await conexao.query('SHOW INDEX FROM tb_d11_aluno'); console.log(''); console.log('--- os indices que a chave estrangeira criou ---'); for (const i of indices) { console.log(' ' + String(i.Key_name).padEnd(16) + ' coluna ' + i.Column_name + ' | unico: ' + (i.Non_unique === '0' ? 'sim' : 'nao')); } console.log(' PRIMARY e a chave primaria; fk_d11_aluno_curso e o indice da chave estrangeira.'); console.log(' ele nao precisou de `INDEX` separado: o banco cria o indice da FK sozinho.'); // --- 8. normalizacao, em uma frase --- console.log(''); console.log('--- o que a chave estrangeira faz pela normalizacao ---'); console.log('Guardar `nm_curso` dentro de tb_d11_aluno repetiria o mesmo texto em toda'); console.log('linha: mudar o nome do curso exigiria um UPDATE em N linhas, e duas'); console.log('linhas poderiam ficar com nomes diferentes. A chave estrangeira guarda o'); console.log('ID, e o nome fica em UM lugar so — a 3a forma normal.'); } finally { await conexao.end(); console.log(''); console.log('conexao encerrada com end().'); } } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
--- 3 cursos e 4 alunos ---
curso 1: Node.js
curso 2: MySQL
curso 3: HTTP
aluno 1: Ana -> curso 1
aluno 2: Bruno -> curso 1
aluno 3: Carla -> curso 2
aluno 4: Diego -> curso 3
--- os tres relacionamentos ---
um para muitos: 1 curso -> N alunos. tb_d11_aluno.id_curso aponta para tb_d11_curso.id
Node.js -> 2 aluno(s)
MySQL -> 1 aluno(s)
HTTP -> 1 aluno(s)
(o `LEFT JOIN` mantem o curso que nao tem aluno nenhum — o `GROUP BY` conta)
muitos para um: N alunos -> 1 curso. E o mesmo desenho, visto do outro lado
`id_curso` em tb_d11_aluno e a chave estrangeira; `id` em tb_d11_curso e a chave primaria.
muitos para muitos: N alunos <-> N cursos, e precisa de uma TERCEIRA tabela.
Nem tb_d11_aluno nem tb_d11_curso aguenta duas chaves estrangeiras ao mesmo tempo,
entao a tabela associativa guarda os dois lados:
as inscricoes:
Ana -> MySQL em 2026-05-04
Ana -> Node.js em 2026-05-04
Bruno -> Node.js em 2026-05-11
`Ana` esta em dois cursos: e a assinatura de um muitos para muitos.
A `PRIMARY KEY (id_aluno, id_curso)` composta impede a inscricao repetida:
erro esperado: ER_DUP_ENTRY - a mesma inscricao ja existe
--- ON DELETE: as tres regras ---
CASCADE | apagar o pai apaga os filhos
SET NULL | apagar o pai zera a coluna do filho (exige coluna NULL)
RESTRICT | apagar o pai e RECUSADO enquanto houver filho (o padrao)
NO ACTION | igual ao RESTRICT na maioria dos casos
--- medindo o SET NULL ---
alunos do curso 3 antes: 1
alunos do curso 3 depois: 0
o aluno que era do curso 3: {"nm_aluno":"Diego","id_curso":null}
o curso sumiu, o aluno ficou, e o id_curso dele virou NULL.
e por isso que `SET NULL` exige coluna NULL: uma coluna NOT NULL nao
tem como receber o NULL que a regra pede.
--- ON UPDATE CASCADE ---
tb_d11_curso.id 2 virou 50; os alunos que apontavam para 2:
antes: Carla=2
depois: Carla=50
o CASCADE atualizou o filho sozinho. Sem ele, o UPDATE seria recusado.
--- integridade referencial: o banco recusando ---
(a) filho apontando para pai inexistente -> recusado, code ER_NO_REFERENCED_ROW_2
(b) dois cursos com o mesmo id -> recusado, code ER_DUP_ENTRY
o `ON UPDATE CASCADE` propagou o id novo para a Carla e, em seguida, o
banco viu que dois cursos teriam o mesmo id, e recusou o segundo.
(c) apagar o pai com filho, sob RESTRICT -> recusado, code ER_ROW_IS_REFERENCED_2
o mesmo DELETE sob CASCADE teria apagado o filho tambem, sem erro.
os tres codes sao diferentes, e e por isso que o `erro.code` e o que o
Node compara: a mensagem muda entre versoes do servidor.
--- os indices que a chave estrangeira criou ---
PRIMARY coluna id | unico: nao
fk_d11_aluno_curso coluna id_curso | unico: nao
PRIMARY e a chave primaria; fk_d11_aluno_curso e o indice da chave estrangeira.
ele nao precisou de `INDEX` separado: o banco cria o indice da FK sozinho.
--- o que a chave estrangeira faz pela normalizacao ---
Guardar `nm_curso` dentro de tb_d11_aluno repetiria o mesmo texto em toda
linha: mudar o nome do curso exigiria um UPDATE em N linhas, e duas
linhas poderiam ficar com nomes diferentes. A chave estrangeira guarda o
ID, e o nome fica em UM lugar so — a 3a forma normal.
conexao encerrada com end().
INNER JOIN, LEFT JOIN e agregação
INNER JOIN, LEFT JOIN e agregação
O JOIN junta duas tabelas pelo que elas têm em comum. A escolha entre INNER,
LEFT e RIGHT é uma pergunta sobre **o que acontece com a linha que não tem
par**: some, ou fica com as colunas do outro lado vazias.
SELECT c.nm_cliente, p.vl_total FROM tb_cliente c LEFT JOIN tb_pedido p ON p.id_cliente = c.id;
Junção é o que traz dados de outra tabela para a mesma linha. Sem ela, quem
precisa do nome do cliente e do total do pedido faz duas consultas e emenda as
duas em JavaScript — e a emenda quebra no primeiro registro que não tiver par.
O JOIN faz a emenda dentro do banco: é o que permite
trazer dados de outra tabela para a mesma linha, sem volta ao Node no caminho.
INNER, LEFT e RIGHT
| JOIN | Linha sem par |
|---|---|
INNER JOIN | some do resultado |
LEFT JOIN | fica, com as colunas do outro lado em NULL |
RIGHT JOIN | o LEFT com as tabelas trocadas |
FULL JOIN | não existe no MySQL |
Sobre o mesmo conjunto de dados — uma cliente sem nenhum pedido — o INNER
devolve 3 linhas e o LEFT devolve 4, com a cliente sem par e id_pedido e
vl_total em NULL. O NULL no lugar do valor é a assinatura do LEFT: a
linha existe, o par não.
RIGHT JOIN é raro, e o motivo é prático: a convenção de escrita é "da tabela que
você quer inteira, a esquerda". SQLite não tem RIGHT; o desenho equivalente é
reescrever como LEFT com as tabelas trocadas. FULL JOIN se simula com a
união de dois LEFT JOIN.
WHERE corta, não o tipo de JOIN
LEFT JOIN tb_pedido p ON p.id_cliente = c.id WHERE p.id IS NOT NULL -- virou INNER JOIN
O filtro é o que corta, e não o JOIN. É por isso que WHERE numa coluna do lado
direito do LEFT anula o LEFT — e é o defeito mais comum de relatório que
"some" com um registro que deveria estar lá.
GROUP BY: o JOIN virando contagem
GROUP BY agrupa as linhas que compartilham o mesmo valor, e a função de
agregação diz o que fazer com cada grupo.
SELECT c.nm_cliente, COUNT(p.id) AS pedidos, SUM(p.vl_total) AS total_gasto FROM tb_cliente c LEFT JOIN tb_pedido p ON p.id_cliente = c.id GROUP BY c.id, c.nm_cliente, c.cidade;
Com LEFT JOIN, a cliente sem pedido entra no grupo com 0 e total NULL. Com
INNER, ela nem entraria — e é por isso que o total geral muda com o tipo de
JOIN quando existe registro sem par de um dos lados.
A COUNT com GROUP BY conta, e a mesma consulta com SUM no lugar do COUNT
faz a soma por grupo. As duas precisam do agrupamento para fazer sentido:
SUM sem GROUP BY devolve o total geral da tabela inteira, que quase nunca é
o que a tela quer.
SELECT p.id_cliente, COUNT(*) AS qtd_pedidos, SUM(p.vl_total) AS total FROM tb_pedido p GROUP BY p.id_cliente;
As duas são COUNT com GROUP BY no sentido de que as duas precisam do
agrupamento para fazer sentido: SUM sem GROUP BY devolve o total geral da
tabela inteira, que quase nunca é o que a tela quer. O COALESCE(SUM(...), 0)
resolve o NULL do grupo vazio, e sem ele a linha da cliente sem pedido volta
com total null no JSON — que o front-end precisa tratar.
ONLY_FULL_GROUP_BY e o sql_mode
Com ONLY_FULL_GROUP_BY no sql_mode, o banco recusa a consulta que pega
coluna não agrupada. O modo muda entre MySQL, MariaDB, versão e instalação — e é
por isso que ele se lê (SELECT @@sql_mode) em vez de se supor.
A forma que passa em qualquer modo é listar o id no GROUP BY: ele é único,
então as outras colunas são só rótulo.
HAVING filtra grupo, WHERE filtra linha
| Cláusula | Filtra | Roda |
|---|---|---|
WHERE | linha, antes do agrupamento | depois do JOIN |
HAVING | grupo, depois do agrupamento | depois do GROUP BY |
WHERE total_gasto > 200 não existe: nessa altura a coluna ainda não foi
calculada. O HAVING roda depois e por isso pode usar COUNT e SUM:
GROUP BY c.id, c.nm_cliente HAVING COUNT(p.id) > 1
A ordem de execução do SQL é FROM → JOIN → WHERE → GROUP BY → HAVING →
SELECT → ORDER BY → LIMIT.
O índice que o JOIN usa
A chave estrangeira cria o índice do lado do filho, e o ON compara esse índice
com a chave primária do pai. Sem FK e sem INDEX, o JOIN vira um nested loop
que compara cada linha com todas as outras — em tabela grande, é a diferença
entre milissegundos e minutos.
EXPLAIN diz o que o planejador fez:
| Campo | O que diz |
|---|---|
type | ALL é varredura inteira da tabela; ref é busca por índice |
key | o índice usado — NULL significa que nenhum foi usado |
rows | quantas linhas ele estima ler |
Extra | Using where, Using temporary (agrupamento), Using filesort |
Exemplo
'use strict'; // Exemplo da aula 2 do dia 11: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN` e `GROUP BY`. // // O `JOIN` junta duas tabelas pelo que elas tem em comum. A escolha entre // `INNER`, `LEFT` e `RIGHT` e uma pergunta sobre o que acontece com a linha que // NAO tem par: some, ou fica com as colunas do outro lado vazias. // // E e essa diferenca que o exemplo mede. Ele monta um caso onde tres linhas do // lado "um" e duas do lado "muitos", uma delas sem par de cada lado, e mostra as // tres juncoes lado a lado sobre a MESMA tabela. A diferenca entre elas fica // visivel em numeros, e nao em descricao. // // O `GROUP BY` vem depois, e e o que transforma o JOIN em contagem: e a // agregacao que responde "quantos", e nao "quais". const mysql = require('mysql2/promise'); async function main() { const conexao = await mysql.createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, }); try { await conexao.query('DROP TABLE IF EXISTS tb_pedido'); await conexao.query('DROP TABLE IF EXISTS tb_cliente'); await conexao.query(` CREATE TABLE tb_cliente ( id INT AUTO_INCREMENT PRIMARY KEY, nm_cliente VARCHAR(40) NOT NULL, cidade VARCHAR(30) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await conexao.query(` CREATE TABLE tb_pedido ( id INT AUTO_INCREMENT PRIMARY KEY, id_cliente INT NOT NULL, vl_total DECIMAL(8,2) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); // Três clientes e dois pedidos: a Ana tem dois pedidos, a Bruno tem um, e a // Carla NÃO tem nenhum. Do lado do pedido, todos têm cliente — essa // assimetria é o que faz a diferença entre INNER e LEFT aparecer. await conexao.query('INSERT INTO tb_cliente (nm_cliente, cidade) VALUES (?, ?), (?, ?), (?, ?)', ['Ana', 'Sao Paulo', 'Bruno', 'Curitiba', 'Carla', 'Recife']); await conexao.query('INSERT INTO tb_pedido (id_cliente, vl_total) VALUES (?, ?), (?, ?), (?, ?)', [1, 100.00, 1, 250.50, 2, 75.25]); console.log('--- 3 clientes, 3 pedidos: Ana tem 2, Bruno tem 1, Carla tem 0 ---'); const [clientes] = await conexao.query('SELECT id, nm_cliente, cidade FROM tb_cliente ORDER BY id'); const [pedidos] = await conexao.query('SELECT id, id_cliente, vl_total FROM tb_pedido ORDER BY id'); for (const c of clientes) console.log(' cliente ' + c.id + ': ' + c.nm_cliente + ' (' + c.cidade + ')'); for (const p of pedidos) console.log(' pedido ' + p.id + ': cliente ' + p.id_cliente + ', total ' + p.vl_total); // --- 1. `INNER JOIN`: só o que tem par dos dois lados --- // // A `Carla` não aparece: ela não tem pedido. E a linha sem par "desaparece" — // o `INNER JOIN` não devolve coluna `NULL`, ele simplesmente não devolve a // linha. const [inner] = await conexao.query(` SELECT c.nm_cliente, c.cidade, p.id AS id_pedido, p.vl_total FROM tb_cliente c INNER JOIN tb_pedido p ON p.id_cliente = c.id ORDER BY c.nm_cliente, p.id`); console.log(''); console.log('--- INNER JOIN: ' + inner.length + ' linha(s) ---'); for (const l of inner) { console.log(' ' + l.nm_cliente.padEnd(7) + ' | ' + l.cidade.padEnd(10) + ' | pedido ' + l.id_pedido + ' | ' + l.vl_total); } console.log(' a Carla sumiu: ela nao tem pedido, e o INNER so devolve o que tem par.'); // --- 2. `LEFT JOIN`: o lado esquerdo vem inteiro --- // // A `Carla` aparece, e as colunas do pedido dela vem `NULL`. É essa a // diferença: o `LEFT JOIN` não esconde a linha sem par, ele mostra ela com // buraco. E é por isso que `LEFT JOIN` é o certo para "todos os clientes e o // que cada um comprou", e `INNER` é o certo para "os pedidos que existem". const [left] = await conexao.query(` SELECT c.nm_cliente, c.cidade, p.id AS id_pedido, p.vl_total FROM tb_cliente c LEFT JOIN tb_pedido p ON p.id_cliente = c.id ORDER BY c.nm_cliente, p.id`); console.log(''); console.log('--- LEFT JOIN: ' + left.length + ' linha(s) ---'); for (const l of left) { console.log(' ' + l.nm_cliente.padEnd(7) + ' | ' + l.cidade.padEnd(10) + ' | pedido ' + (l.id_pedido === null ? 'null' : l.id_pedido) + ' | ' + (l.vl_total === null ? 'null' : l.vl_total)); } console.log(' a Carla apareceu, com id_pedido e vl_total NULL.'); console.log(' o NULL no lugar do valor e a assinatura do LEFT JOIN: a linha existe,'); console.log(' o par nao. E e por isso que `WHERE p.id IS NOT NULL` transforma um'); console.log(' LEFT JOIN em INNER JOIN — e por isso que o filtro WHERE TRAZ a Carla de volta.'); // A prova de que o filtro WHERE e o que muda a contagem, e nao o LEFT: const [leftFiltrado] = await conexao.query(` SELECT c.nm_cliente, p.id AS id_pedido FROM tb_cliente c LEFT JOIN tb_pedido p ON p.id_cliente = c.id WHERE p.id IS NOT NULL ORDER BY c.nm_cliente, p.id`); console.log(''); console.log(' o MESMO LEFT JOIN com `WHERE p.id IS NOT NULL`: ' + leftFiltrado.length + ' linha(s)'); for (const l of leftFiltrado) console.log(' ' + l.nm_cliente + ' | pedido ' + l.id_pedido); console.log(' virou um INNER JOIN. O `WHERE` e o que corta, e nao o tipo de JOIN.'); // --- 3. `RIGHT JOIN` e a troca de lado --- // // `RIGHT JOIN` e `LEFT JOIN` com as tabelas trocadas. O MySQL aceita; o // detalhe é que ele é raro, e o motivo é prático: a convention de escrita é // "da tabela que você quer inteira, a esquerda". const [right] = await conexao.query(` SELECT c.nm_cliente, p.id AS id_pedido FROM tb_pedido p RIGHT JOIN tb_cliente c ON p.id_cliente = c.id ORDER BY c.nm_cliente, p.id`); console.log(''); console.log('--- RIGHT JOIN: ' + right.length + ' linha(s) ---'); for (const l of right) { console.log(' ' + l.nm_cliente.padEnd(7) + ' | pedido ' + (l.id_pedido === null ? 'null' : l.id_pedido)); } console.log(' mesmo resultado do LEFT, so que comecando pelo pedido.'); console.log(' MySQL aceita RIGHT; PostgreSQL tambem; SQLite nao tem RIGHT, e o'); console.log(' desenho equivalente e reescrever como LEFT com as tabelas trocadas.'); // --- 4. `FULL JOIN` não existe no MySQL --- // // O `FULL JOIN` traz as duas direções: linha sem par dos dois lados. O MySQL // não implementa. A simulação padrão é a união de dois `LEFT JOIN`: console.log(''); console.log('--- FULL JOIN: nao existe no MySQL ---'); const [fullSimulado] = await conexao.query(` SELECT c.nm_cliente, c.cidade, p.id AS id_pedido, p.vl_total FROM tb_cliente c LEFT JOIN tb_pedido p ON p.id_cliente = c.id WHERE c.nm_cliente IS NOT NULL UNION SELECT c.nm_cliente, c.cidade, p.id AS id_pedido, p.vl_total FROM tb_pedido p LEFT JOIN tb_cliente c ON p.id_cliente = c.id WHERE c.nm_cliente IS NULL`); console.log(' o mesmo resultado de um LEFT JOIN, porque aqui nao ha pedido sem cliente:'); console.log(' linhas:', fullSimulado.length); console.log(' (a FK impede o pedido sem cliente; sem a FK, a segunda metade do UNION'); console.log(' traria essas linhas, e e ai que o FULL JOIN faria diferenca)'); // --- 5. `GROUP BY`: o JOIN virando contagem --- // // `GROUP BY` agrupa as linhas que compartilham o mesmo valor, e a funcao de // agregacao diz o que fazer com cada grupo. E a resposta para "quantos" — o // `COUNT` da aula 9, mas contando por grupo. const [contagem] = await conexao.query(` SELECT c.nm_cliente, c.cidade, COUNT(p.id) AS pedidos, SUM(p.vl_total) AS total_gasto FROM tb_cliente c LEFT JOIN tb_pedido p ON p.id_cliente = c.id GROUP BY c.id, c.nm_cliente, c.cidade ORDER BY total_gasto DESC`); console.log(''); console.log('--- GROUP BY com agregacao ---'); console.log(' ' + 'cliente'.padEnd(8) + '| ' + 'cidade'.padEnd(10) + '| pedidos | total gasto'); for (const l of contagem) { console.log(' ' + l.nm_cliente.padEnd(8) + '| ' + l.cidade.padEnd(10) + '| ' + String(l.pedidos).padStart(7) + ' | ' + (l.total_gasto === null ? 'null' : l.total_gasto)); } console.log(' a Carla tem 0 pedidos e total null: e o LEFT preservando ela no grupo.'); console.log(' com INNER JOIN, a Carla nem entraria no grupo, e o total geral mudaria.'); // A prova: o total geral com LEFT e com INNER, e o quanto a linha sem par pesa. const [totalLeft] = await conexao.query(` SELECT COUNT(p.id) AS pedidos, COALESCE(SUM(p.vl_total), 0) AS total FROM tb_cliente c LEFT JOIN tb_pedido p ON p.id_cliente = c.id`); const [totalInner] = await conexao.query(` SELECT COUNT(p.id) AS pedidos, SUM(p.vl_total) AS total FROM tb_cliente c INNER JOIN tb_pedido p ON p.id_cliente = c.id`); console.log(''); console.log(' o total geral muda com o tipo de JOIN:'); console.log(' LEFT -> ' + totalLeft[0].pedidos + ' pedido(s), total ' + totalLeft[0].total); console.log(' INNER -> ' + totalInner[0].pedidos + ' pedido(s), total ' + totalInner[0].total); console.log(' neste caso os numeros de pedido batem (a Carla nao tinha nenhum).'); console.log(' Eles divergiriam se houvesse pedido sem cliente — que a FK impede, mas'); console.log(' que acontece em qualquer tabela sem FK.'); // --- 6. `ONLY_FULL_GROUP_BY` e o `sql_mode` --- // // A coluna do `GROUP BY` e a que identifica o grupo; as outras precisam ser // agregadas. Se o `sql_mode` tiver `ONLY_FULL_GROUP_BY`, o banco RECUSA a // consulta que pega coluna nao agrupada — e a forma de descobrir e LER o modo, // nao supor. O exemplo le, e o que ele le decide o que acontece abaixo. const [modo] = await conexao.query('SELECT @@sql_mode AS modo'); const estrito = modo[0].modo.includes('ONLY_FULL_GROUP_BY'); console.log(''); console.log('--- ONLY_FULL_GROUP_BY ---'); console.log(' sql_mode deste servidor:', modo[0].modo); console.log(' tem ONLY_FULL_GROUP_BY?', estrito); try { await conexao.query( 'SELECT c.cidade, p.vl_total FROM tb_cliente c JOIN tb_pedido p ON p.id_cliente = c.id GROUP BY c.cidade' ); console.log(' pegou coluna fora do GROUP BY -> ACEITOU (o modo acima nao e estrito)'); } catch (erro) { console.error(' ' + erro.code + ': ' + erro.message); console.log(' pegou coluna fora do GROUP BY -> recusado, code', erro.code); } console.log(''); console.log(' O mesmo `sql_mode` desta maquina e o que a aula 1 mediu com `@@sql_mode`.'); console.log(' Ele muda entre MySQL, MariaDB, versao e instalacao — e por isso que a'); console.log(' forma segura de escrever e `GROUP BY c.id, c.nm_cliente, c.cidade`: o id e'); console.log(' unico, entao os outros dois e so rotulo, e a consulta passa em qualquer modo.'); console.log(' A consulta com so `GROUP BY c.cidade` e aceita aqui e seria recusada num'); console.log(' servidor com ONLY_FULL_GROUP_BY: o mesmo codigo, dois comportamentos.'); // --- 7. `HAVING`: filtrar depois de agrupar --- // // `WHERE` filtra LINHAS, antes do agrupamento. `HAVING` filtra GRUPOS, depois. // E a diferença que importa: `WHERE total_gasto > 200` nao existe, porque // `total_gasto` ainda nao foi calculado quando o `WHERE` roda. const [having] = await conexao.query(` SELECT c.nm_cliente, COUNT(p.id) AS pedidos, SUM(p.vl_total) AS total_gasto FROM tb_cliente c LEFT JOIN tb_pedido p ON p.id_cliente = c.id GROUP BY c.id, c.nm_cliente HAVING COUNT(p.id) > 1 ORDER BY total_gasto DESC`); console.log(''); console.log('--- HAVING: filtrar o grupo, nao a linha ---'); for (const l of having) { console.log(' ' + l.nm_cliente + ': ' + l.pedidos + ' pedido(s), total ' + l.total_gasto); } console.log(' so a Ana passou (2 pedidos). O `HAVING COUNT(p.id) > 1` roda DEPOIS'); console.log(' do agrupamento, e por isso que ele pode usar COUNT e SUM.'); console.log(''); console.log(' a ordem de execucao do SQL:'); console.log(' FROM -> JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT'); console.log(' e por isso que o `WHERE` nao aceita `total_gasto > 200`: nessa altura'); console.log(' a coluna ainda nao existe como valor calculado.'); // --- 8. a mesma pergunta, de tres jeitos --- console.log(''); console.log('--- a mesma pergunta ("clientes que compraram mais de 200"), 3 jeitos ---'); const [porHaving] = await conexao.query(` SELECT c.nm_cliente, SUM(p.vl_total) AS t FROM tb_cliente c JOIN tb_pedido p ON p.id_cliente = c.id GROUP BY c.id, c.nm_cliente HAVING SUM(p.vl_total) > 200`); const [porSub] = await conexao.query(` SELECT nm_cliente, t FROM ( SELECT c.nm_cliente, SUM(p.vl_total) AS t FROM tb_cliente c JOIN tb_pedido p ON p.id_cliente = c.id GROUP BY c.id, c.nm_cliente ) AS agregado WHERE t > 200`); const [porJoin] = await conexao.query(` SELECT c.nm_cliente, (SELECT SUM(p.vl_total) FROM tb_pedido p WHERE p.id_cliente = c.id) AS t FROM tb_cliente c WHERE (SELECT SUM(p.vl_total) FROM tb_pedido p WHERE p.id_cliente = c.id) > 200`); console.log(' HAVING ->', porHaving.map((l) => l.nm_cliente).join(', ') || '(ninguem)'); console.log(' subconsulta ->', porSub.map((l) => l.nm_cliente).join(', ') || '(ninguem)'); console.log(' subconsulta no WHERE ->', porJoin.map((l) => l.nm_cliente).join(', ') || '(ninguem)'); console.log(' os tres dao o mesmo resultado. O `HAVING` e o legivel; o subselect e o'); console.log(' unico que aceita o mesmo filtro no WHERE sem repetir a agregacao.'); // --- 9. a chave para o JOIN: o indice --- // // A FK cria o indice do lado do filho, e o `ON` compara esse indice com a // chave primaria do pai. Sem a FK e sem o `INDEX`, o `JOIN` vira um // "nested loop" que compara cada linha com todas as outras: em tabela grande, // e a diferenca entre milissegundos e minutos. const [indicesPedido] = await conexao.query('SHOW INDEX FROM tb_pedido'); console.log(''); console.log('--- o indice que o JOIN usa ---'); for (const i of indicesPedido) { console.log(' ' + String(i.Key_name).padEnd(12) + ' coluna ' + i.Column_name); } console.log(' PRIMARY e a chave primaria de tb_pedido; o `id_cliente` nao tem indice'); console.log(' porque esta tabela foi criada SEM chave estrangeira — e o exemplo mede'); console.log(' justamente esse caso: o JOIN funciona, e so fica mais lento quando a'); console.log(' tabela cresce. A FK da aula 1 cria esse indice sozinha.'); const [explain] = await conexao.query( 'EXPLAIN SELECT c.nm_cliente FROM tb_cliente c JOIN tb_pedido p ON p.id_cliente = c.id' ); console.log(''); console.log(' o que o planejador viu (EXPLAIN):'); for (const k of ['table', 'type', 'possible_keys', 'key', 'rows', 'Extra']) { if (explain[0][k] !== undefined) console.log(' ' + k.padEnd(14) + ' = ' + explain[0][k]); } console.log(' `key: NULL` diz que o planejador NAO usou indice: leu uma tabela e'); console.log(' procurou a outra linha a linha. Com o indice da FK, viraria `ref`.'); } finally { await conexao.end(); console.log(''); console.log('conexao encerrada com end().'); } } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
--- 3 clientes, 3 pedidos: Ana tem 2, Bruno tem 1, Carla tem 0 ---
cliente 1: Ana (Sao Paulo)
cliente 2: Bruno (Curitiba)
cliente 3: Carla (Recife)
pedido 1: cliente 1, total 100.00
pedido 2: cliente 1, total 250.50
pedido 3: cliente 2, total 75.25
--- INNER JOIN: 3 linha(s) ---
Ana | Sao Paulo | pedido 1 | 100.00
Ana | Sao Paulo | pedido 2 | 250.50
Bruno | Curitiba | pedido 3 | 75.25
a Carla sumiu: ela nao tem pedido, e o INNER so devolve o que tem par.
--- LEFT JOIN: 4 linha(s) ---
Ana | Sao Paulo | pedido 1 | 100.00
Ana | Sao Paulo | pedido 2 | 250.50
Bruno | Curitiba | pedido 3 | 75.25
Carla | Recife | pedido null | null
a Carla apareceu, com id_pedido e vl_total NULL.
o NULL no lugar do valor e a assinatura do LEFT JOIN: a linha existe,
o par nao. E e por isso que `WHERE p.id IS NOT NULL` transforma um
LEFT JOIN em INNER JOIN — e por isso que o filtro WHERE TRAZ a Carla de volta.
o MESMO LEFT JOIN com `WHERE p.id IS NOT NULL`: 3 linha(s)
Ana | pedido 1
Ana | pedido 2
Bruno | pedido 3
virou um INNER JOIN. O `WHERE` e o que corta, e nao o tipo de JOIN.
--- RIGHT JOIN: 4 linha(s) ---
Ana | pedido 1
Ana | pedido 2
Bruno | pedido 3
Carla | pedido null
mesmo resultado do LEFT, so que comecando pelo pedido.
MySQL aceita RIGHT; PostgreSQL tambem; SQLite nao tem RIGHT, e o
desenho equivalente e reescrever como LEFT com as tabelas trocadas.
--- FULL JOIN: nao existe no MySQL ---
o mesmo resultado de um LEFT JOIN, porque aqui nao ha pedido sem cliente:
linhas: 4
(a FK impede o pedido sem cliente; sem a FK, a segunda metade do UNION
traria essas linhas, e e ai que o FULL JOIN faria diferenca)
--- GROUP BY com agregacao ---
cliente | cidade | pedidos | total gasto
Ana | Sao Paulo | 2 | 350.50
Bruno | Curitiba | 1 | 75.25
Carla | Recife | 0 | null
a Carla tem 0 pedidos e total null: e o LEFT preservando ela no grupo.
com INNER JOIN, a Carla nem entraria no grupo, e o total geral mudaria.
o total geral muda com o tipo de JOIN:
LEFT -> 3 pedido(s), total 425.75
INNER -> 3 pedido(s), total 425.75
neste caso os numeros de pedido batem (a Carla nao tinha nenhum).
Eles divergiriam se houvesse pedido sem cliente — que a FK impede, mas
que acontece em qualquer tabela sem FK.
--- ONLY_FULL_GROUP_BY ---
sql_mode deste servidor: IGNORE_SPACE,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
tem ONLY_FULL_GROUP_BY? false
pegou coluna fora do GROUP BY -> ACEITOU (o modo acima nao e estrito)
O mesmo `sql_mode` desta maquina e o que a aula 1 mediu com `@@sql_mode`.
Ele muda entre MySQL, MariaDB, versao e instalacao — e por isso que a
forma segura de escrever e `GROUP BY c.id, c.nm_cliente, c.cidade`: o id e
unico, entao os outros dois e so rotulo, e a consulta passa em qualquer modo.
A consulta com so `GROUP BY c.cidade` e aceita aqui e seria recusada num
servidor com ONLY_FULL_GROUP_BY: o mesmo codigo, dois comportamentos.
--- HAVING: filtrar o grupo, nao a linha ---
Ana: 2 pedido(s), total 350.50
so a Ana passou (2 pedidos). O `HAVING COUNT(p.id) > 1` roda DEPOIS
do agrupamento, e por isso que ele pode usar COUNT e SUM.
a ordem de execucao do SQL:
FROM -> JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT
e por isso que o `WHERE` nao aceita `total_gasto > 200`: nessa altura
a coluna ainda nao existe como valor calculado.
--- a mesma pergunta ("clientes que compraram mais de 200"), 3 jeitos ---
HAVING -> Ana
subconsulta -> Ana
subconsulta no WHERE -> Ana
os tres dao o mesmo resultado. O `HAVING` e o legivel; o subselect e o
unico que aceita o mesmo filtro no WHERE sem repetir a agregacao.
--- o indice que o JOIN usa ---
PRIMARY coluna id
PRIMARY e a chave primaria de tb_pedido; o `id_cliente` nao tem indice
porque esta tabela foi criada SEM chave estrangeira — e o exemplo mede
justamente esse caso: o JOIN funciona, e so fica mais lento quando a
tabela cresce. A FK da aula 1 cria esse indice sozinha.
o que o planejador viu (EXPLAIN):
table = c
type = ALL
possible_keys = PRIMARY
key = null
rows = 3
Extra =
`key: NULL` diz que o planejador NAO usou indice: leu uma tabela e
procurou a outra linha a linha. Com o indice da FK, viraria `ref`.
conexao encerrada com end().