Dia 11 — JOIN

Informatica · Conteudo · publicado em 30/09/2026
Aula 1

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ãoExemplo
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

TipoDesenhoOnde fica a FK
um para muitos1 curso → N alunostb_aluno.id_curso
muitos para umN pedidos → 1 clienteo mesmo desenho, visto do outro lado
muitos para muitosN alunos ↔ N cursostabela 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

RegraApagar o pai faz
CASCADEapaga os filhos também
SET NULLzera a coluna do filho — exige coluna NULL
RESTRICTrecusa enquanto houver filho (o padrão)
NO ACTIONigual 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çãoerro.code
filho apontando para pai inexistenteER_NO_REFERENCED_ROW_2
dois registros com o mesmo id de paiER_DUP_ENTRY
apagar o pai com filho, sob RESTRICTER_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().
Aula 2

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

JOINLinha sem par
INNER JOINsome do resultado
LEFT JOINfica, com as colunas do outro lado em NULL
RIGHT JOINo LEFT com as tabelas trocadas
FULL JOINnã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áusulaFiltraRoda
WHERElinha, antes do agrupamentodepois do JOIN
HAVINGgrupo, depois do agrupamentodepois 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:

CampoO que diz
typeALL é varredura inteira da tabela; ref é busca por índice
keyo índice usado — NULL significa que nenhum foi usado
rowsquantas linhas ele estima ler
ExtraUsing 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().