Dia 9 — SQL: inserir e consultar

Informatica · Conteudo · publicado em 30/09/2026
Dia 9 de 16

SQL: inserir e consultar

Aula 1

INSERT: gravar dados

INSERT: gravar dados

INSERT INTO ... VALUES grava uma linha. O que o Node recebe de volta **não é o

dado gravado**: é um objeto de metadados, e é ele que prova que a gravação

aconteceu.

const [resultado] = await conexao.query(
  'INSERT INTO tb_inscricao (nm_aluno, turma, vl_nota) VALUES (?, ?, ?)',
  ['Ana', 3, 8.5]
);
resultado.affectedRows;   // 1
resultado.insertId;       // 1

Um INSERT INTO ... VALUES é um inserir registro: uma linha nova, com todos os

valores escritos na própria frase. A resposta à pergunta "qual foi o último id

gravado" aparece em dois lugares com o mesmo número — o campo insertId do

objeto que o driver devolve, e a função LAST_INSERT_ID() do lado do banco, que

consulta a sessão e não exige viagem nenhuma. São o mesmo valor, e o motivo de

existirem os dois é histórico: LAST_INSERT_ID() já estava no MySQL muito antes

de existir qualquer driver em JavaScript.

O objeto que volta

CampoO que é
affectedRowsquantas linhas entraram
insertIdo id gravado (o que o banco gerou, ou o que veio no INSERT)
warningStatusquantos avisos — não erros
changedRowsquantas mudaram de valor
infoo texto do servidor, como "Rows matched: 1 Changed: 1"
fieldCountquantas colunas o comando devolve (0 fora do SELECT)

warningCount não existe: o nome do campo é warningStatus. E o INSERT

que deu ER_DUP_ENTRY nunca chega aqui — o erro lança, e a exceção tem

code e sqlState.

insertId e a pegadinha

O auto incremento na prática é isto: a coluna id INT AUTO_INCREMENT não recebe

valor no INSERT, e o banco sorteia o próximo número da fila. Por isso o

insertId é sempre preenchido aqui — e é por isso que ele vale mais do que um

número: ele é a prova de que o banco gravou, mesmo quando o INSERT não gravou

(INSERT IGNORE) e mesmo quando o affectedRows é 0.

A intuição diz que insertId volta 0 quando o INSERT traz o id pronto,

porque "o banco não gerou nada". Medido: o banco devolve o id gravado, mesmo

vindo do chamador. Por isso a prova de que gravou continua sendo o SELECT

depois — e não o número do affectedRows.

Várias linhas em um comando só

INSERT INTO tb_inscricao (nm_aluno, turma, vl_nota) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)

Inserir vários registros nesse formato é mais rápido que três chamadas: uma ida ao

servidor só, e o banco grava em bloco. O insertId devolvido é o do primeiro

inserted; os outros seguem +1. O affectedRows é 3.

INSERT IGNORE: a duplicata vira zero

Duplicata em chave primária dá ER_DUP_ENTRY. Com IGNORE, o banco ignora a

linha e segue:

É exatamente por isso que IGNORE é perigoso em gravação: **o código acha que

gravou, e o dado não foi salvo.** Quem usa IGNORE em INSERT de verdade está

aceitando perder dado em silêncio.

ON DUPLICATE KEY UPDATE

Faz o INSERT virar UPDATE quando a chave bate. O affectedRows conta a linha

nos dois casos, então 2 significa "atualizou" e 1 significa "inseriu".

O caso que derruba a intuição: **o mesmo comando com o mesmo valor devolve

affectedRows: 1**, não 0. Quem precisa saber se o valor mudou de fato usa o

changedRows e o info — que no UPDATE dizem "Rows matched: 1 Changed: 0"

quando o valor já era o mesmo.

TRUNCATE devolve affectedRows: 0

Um TRUNCATE apaga a tabela inteira e devolve zero. É o primeiro caso em que o

número engana, e é o motivo de a regra ser: **para contar o que sumiu, conta

antes.**

INSERT ... SELECT

Grava o resultado de uma consulta sem passar por JavaScript:

INSERT INTO tb_aprovado (nm_aluno, vl_nota)
SELECT nm_aluno, vl_nota FROM tb_inscricao WHERE vl_nota >= 7.00

O dado vai de uma tabela para outra direto no banco — a rede entre Node e MySQL

transporta só a instrução, não as linhas. É assim que se faz carga inicial e

migração de dado.

O que query devolve, por tipo de comando

ComandoPrimeiro valorSegundo
SELECT[linhas, campos] — linhas são objetosmetadados das colunas
INSERTaffectedRows, insertId, warningStatusundefined
UPDATEaffectedRows, changedRows, infoundefined
DELETEaffectedRowsundefined
TRUNCATEaffectedRows (sempre 0)undefined

Por isso a desestruturação é sempre [algo, campos]: o primeiro valor é a coisa

que interessa, e o segundo só importa no SELECT.

Exemplo

'use strict';

// Exemplo da aula 1 do dia 9: `INSERT`, e o que o banco devolve.
//
// `INSERT INTO ... VALUES` grava. O que o Node recebe de volta nao e o dado
// gravado: e um objeto de metadados — quantas linhas entraram, qual id o banco
// gerou, quantos avisos. E esse objeto que prova que a gravacao aconteceu.
//
// E o exemplo mede esse objeto em cada caso, em vez de afirmar o que ele vale.
// Duas das medicoes derrubam a intuicao: o `INSERT IGNORE` de uma duplicata devolve
// `affectedRows: 0` com um aviso registrado, e o `ON DUPLICATE KEY UPDATE` com o
// MESMO valor devolve `affectedRows: 1` — nao zero. E por isso que a unica prova
// de gravacao continua sendo o `SELECT` depois.

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(`
      CREATE TABLE IF NOT EXISTS tb_inscricao (
        id        INT AUTO_INCREMENT PRIMARY KEY,
        nm_aluno  VARCHAR(40) NOT NULL,
        turma     INT         NOT NULL,
        vl_nota   DECIMAL(4,2) NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);
    // `TRUNCATE` no COMECO, nunca no fim: e ele que zera o `AUTO_INCREMENT`,
    // e por isso que o `insertId` abaixo comeca sempre em 1.
    await conexao.query('TRUNCATE TABLE tb_inscricao');
    console.log('--- tabela pronta (TRUNCATE zera o auto incremento) ---');

    // --- 1. o INSERT de uma linha, e o objeto que volta ---
    //
    // O `?` e o placeholder. O driver manda o texto do SQL e os valores SEPARADOS,
    // e quem junta tudo e o SERVIDOR. E a aula 13 inteira; aqui so o fato:
    // o valor nunca entra dentro do texto do SQL.
    const [um] = await conexao.query(
      'INSERT INTO tb_inscricao (nm_aluno, turma, vl_nota) VALUES (?, ?, ?)',
      ['Ana', 3, 8.5]
    );
    console.log('');
    console.log('--- INSERT de uma linha: o objeto que volta ---');
    for (const [chave, valor] of Object.entries(um)) {
      console.log('  ' + chave.padEnd(14) + ' = ' + JSON.stringify(valor));
    }
    console.log('');
    console.log('affectedRows: quantas linhas entraram. insertId: o id que o banco gerou.');
    console.log('warningStatus: quantos AVISOS, quantos erros. info: o texto do servidor.');
    console.log('(`warningCount` NAO existe: o nome do campo e `warningStatus`, e ele conta');
    console.log(' aviso — o INSERT que deu ER_DUP_ENTRY nunca chega aqui, porque lancou.)');

    const [confere] = await conexao.query(
      'SELECT id, nm_aluno, turma, vl_nota FROM tb_inscricao WHERE id = ?', [um.insertId]
    );
    console.log('o SELECT com esse id traz:', JSON.stringify(confere[0]));
    console.log('e essa consulta, e nao o `affectedRows`, que e a prova de que gravou.');

    // --- 2. INSERT de varias linhas, um comando so ---
    //
    // `VALUES (?, ?), (?, ?), (?, ?)` grava varias linhas em uma ida ao servidor.
    // Tres linhas num `INSERT` e mais rapido que tres chamadas: uma ida so, e o
    // banco grava em bloco.
    const [varias] = await conexao.query(
      `INSERT INTO tb_inscricao (nm_aluno, turma, vl_nota) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)`,
      ['Bruno', 3, 6.0, 'Carla', 4, 9.25, 'Diego', 3, null]
    );
    console.log('');
    console.log('--- INSERT de tres linhas em um comando so ---');
    console.log('affectedRows:', varias.affectedRows, '<-- tres linhas, uma ida ao servidor');
    console.log('insertId:    ', varias.insertId, '<-- o id do PRIMEIRO inserted; o resto segue +1');

    const [todas] = await conexao.query('SELECT id, nm_aluno, turma FROM tb_inscricao ORDER BY id');
    console.log('o que esta na tabela:');
    for (const linha of todas) {
      console.log('  id ' + linha.id + ' | ' + linha.nm_aluno + ' | turma ' + linha.turma);
    }
    console.log('os ids continuam 1, 2, 3, 4 porque o TRUNCATE do comeco zerou o contador.');

    // --- 3. `insertId` quando o id vem pronto no INSERT ---
    //
    // Medido: o `insertId` volta com o id que foi GRAVADO, mesmo quando o
    // `INSERT` traz o `id` explicito e o banco nao gerou nada. A intuição diz o
    // contrario ("o banco nao gerou, entao devolve 0"), e e por isso que a
    // leitura de `insertId` tem que ser conferida com o SELECT quando o id vem
    // do chamador — em `INSERT IGNORE`, que devolve `affectedRows: 0` e nao
    // grava nada, o `insertId` e a unica pista do que teria sido gravado.
    const [comId] = await conexao.query(
      'INSERT INTO tb_inscricao (id, nm_aluno, turma) VALUES (?, ?, ?)', [99, 'Semente', 1]
    );
    console.log('');
    console.log('--- insertId com o id vindo pronto no INSERT ---');
    console.log('insertId:', comId.insertId, '<-- o id gravado, mesmo tendo vindo do chamador');
    console.log('affectedRows:', comId.affectedRows, '<-- e a linha foi gravada');
    console.log('  a intuição diz que `insertId` seria 0 ("o banco nao gerou nada");');
    console.log('  o banco devolve o id gravado. Por isso a prova continua sendo o SELECT.');

    // --- 4. `INSERT IGNORE`: a duplicata vira affectedRows 0 ---
    //
    // Duplicata em chave primaria vira `ER_DUP_ENTRY`. Com `IGNORE`, o banco
    // ignora a linha e segue: o INSERT "deu certo" com `affectedRows: 0` e um
    // aviso registrado. E exatamente por isso que `IGNORE` e perigoso em gravacao
    // — o codigo acha que gravou, e o dado nao foi salvo.
    const [ignora] = await conexao.query(
      'INSERT IGNORE INTO tb_inscricao (id, nm_aluno, turma) VALUES (?, ?, ?)',
      [99, 'Tentativa', 1]
    );
    console.log('');
    console.log('--- INSERT IGNORE: a duplicata vira affectedRows 0 ---');
    console.log('affectedRows: ', ignora.affectedRows, '<-- zero: nada foi gravado');
    console.log('warningStatus:', ignora.warningStatus, '<-- o banco avisou, e nao parou');
    const [soSemente] = await conexao.query('SELECT nm_aluno FROM tb_inscricao WHERE id = 99');
    console.log('o que ficou na linha 99:', JSON.stringify(soSemente[0]), '<-- o primeiro, nao o novo');

    // --- 5. sem `IGNORE`, a mesma duplicata ---
    console.log('');
    console.log('--- a mesma duplicata SEM IGNORE ---');
    try {
      await conexao.query(
        'INSERT INTO tb_inscricao (id, nm_aluno, turma) VALUES (?, ?, ?)',
        [99, 'Tentativa', 1]
      );
    } catch (erro) {
      console.error(erro.code + ': ' + erro.message);
      console.log('  erro esperado:', erro.code);
      console.log('  o `code` e o que o Node compara; a mensagem muda entre versoes.');
      console.log('  e o `sqlState` tambem: e o codigo padrao ANSI, o mesmo em qualquer SGBD.');
      console.log('  sqlState:', erro.sqlState);
    }

    // --- 6. `ON DUPLICATE KEY UPDATE` ---
    //
    // Faz o `INSERT` virar `UPDATE` quando a chave bate. O `affectedRows` conta a
    // linha nos dois casos, e por isso que `2` significa "atualizou" e `1`
    // significa "inseriu".
    const [upsert] = await conexao.query(
      `INSERT INTO tb_inscricao (id, nm_aluno, turma, vl_nota) VALUES (?, ?, ?, ?)
       ON DUPLICATE KEY UPDATE nm_aluno = VALUES(nm_aluno), vl_nota = VALUES(vl_nota)`,
      [99, 'Atualizado', 1, 7.75]
    );
    console.log('');
    console.log('--- ON DUPLICATE KEY UPDATE ---');
    console.log('affectedRows:', upsert.affectedRows, '<-- 2 significa UPDATE, 1 significa INSERT');
    const [atualizado] = await conexao.query('SELECT nm_aluno, vl_nota FROM tb_inscricao WHERE id = 99');
    console.log('a linha 99 agora e:', JSON.stringify(atualizado[0]));

    // O MESMO comando, agora com o MESMO valor. A intuição manda esperar zero
    // ("nao mudou nada"), e o banco devolve 1. E por isso que a affectedRows
    // nao serve para decidir "o dado foi salvo": quem precisa disso consulta
    // depois, ou compara `changedRows`, que no `UPDATE` distingue as duas coisas.
    const [idem] = await conexao.query(
      `INSERT INTO tb_inscricao (id, nm_aluno, turma, vl_nota) VALUES (?, ?, ?, ?)
       ON DUPLICATE KEY UPDATE nm_aluno = VALUES(nm_aluno), vl_nota = VALUES(vl_nota)`,
      [99, 'Atualizado', 1, 7.75]
    );
    console.log('');
    console.log('o MESMO comando de novo, com o MESMO valor: affectedRows', idem.affectedRows);
    console.log('  a intuicao diz 0 ("nao mudou nada") e o banco devolve 1. Medido, nao suposto.');
    console.log('  e no `UPDATE` quem distingue as duas coisas e o `info` e o `changedRows`:');
    const [upd] = await conexao.query(
      'UPDATE tb_inscricao SET vl_nota = ? WHERE id = ?', [7.75, 99]
    );
    console.log('  UPDATE com o mesmo valor -> affectedRows', upd.affectedRows,
      '| changedRows', upd.changedRows, '| info:', JSON.stringify(upd.info));
    const [upd2] = await conexao.query(
      'UPDATE tb_inscricao SET vl_nota = ? WHERE id = ?', [8.00, 99]
    );
    console.log('  UPDATE mudando o valor   -> affectedRows', upd2.affectedRows,
      '| changedRows', upd2.changedRows, '| info:', JSON.stringify(upd2.info));

    // --- 7. `INSERT ... SELECT` ---
    //
    // Grava o resultado de uma consulta sem passar por JavaScript: o dado vai de
    // uma tabela para outra direto no banco. E assim que se faz carga inicial e
    // migracao de dado sem passar mil linhas pelo Node.
    await conexao.query(`
      CREATE TABLE IF NOT EXISTS tb_aprovado (
        id       INT AUTO_INCREMENT PRIMARY KEY,
        nm_aluno VARCHAR(40) NOT NULL,
        vl_nota  DECIMAL(4,2) NOT NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);
    await conexao.query('TRUNCATE TABLE tb_aprovado');
    const [copia] = await conexao.query(
      'INSERT INTO tb_aprovado (nm_aluno, vl_nota) ' +
      'SELECT nm_aluno, vl_nota FROM tb_inscricao WHERE vl_nota >= 7.00'
    );
    console.log('');
    console.log('--- INSERT ... SELECT: de uma tabela para outra, sem passar pelo Node ---');
    console.log('copiou as notas >= 7.00 | affectedRows:', copia.affectedRows);
    const [aprovados] = await conexao.query('SELECT nm_aluno, vl_nota FROM tb_aprovado ORDER BY nm_aluno');
    for (const a of aprovados) console.log('  ' + a.nm_aluno + ' -> ' + a.vl_nota);
    console.log('o dado foi copiado DENTRO do banco: a rede entre Node e MySQL');
    console.log('transportou so a instrucao, nao as linhas.');

    // --- 8. `TRUNCATE` devolve affectedRows 0 ---
    //
    // Um detalhe que quebra a leitura de "affectedRows = quantas linhas mudaram":
    // `TRUNCATE` apaga todas e devolve zero. O `affectedRows` do `TRUNCATE` nao
    // conta nada, e o e o primeiro caso em que o numero engana.
    const [trunc] = await conexao.query('TRUNCATE TABLE tb_aprovado');
    console.log('');
    console.log('--- TRUNCATE devolve affectedRows 0 ---');
    console.log('affectedRows:', trunc.affectedRows, '<-- apagou a tabela inteira e devolve 0');
    console.log('  e por isso que a aula de DELETE diz: para contar o que sumiu, conta antes.');

    // --- 9. o que `query` devolve por tipo de comando ---
    console.log('');
    console.log('--- o que `query` devolve, por tipo de comando ---');
    console.log('SELECT    -> [linhas, campos]      linhas = array de objetos, campos = metadados');
    console.log('INSERT    -> [resultado, undefined] resultado = affectedRows, insertId, warningStatus');
    console.log('UPDATE    -> [resultado, undefined] resultado = affectedRows, changedRows, info');
    console.log('DELETE    -> [resultado, undefined] affectedRows = quantas sumiram');
    console.log('TRUNCATE  -> [resultado, undefined] affectedRows = 0, sempre');
    console.log('');
    console.log('E por isso que a desestruturacao e sempre [algo, campos]: o primeiro valor');
    console.log('e a coisa que interessa, e o segundo so importa em SELECT, onde sao os');
    console.log('metadados das colunas.');
  } 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

--- tabela pronta (TRUNCATE zera o auto incremento) ---

--- INSERT de uma linha: o objeto que volta ---
  fieldCount     = 0
  affectedRows   = 1
  insertId       = 1
  info           = ""
  serverStatus   = 2
  warningStatus  = 0
  changedRows    = 0

affectedRows: quantas linhas entraram. insertId: o id que o banco gerou.
warningStatus: quantos AVISOS, quantos erros. info: o texto do servidor.
(`warningCount` NAO existe: o nome do campo e `warningStatus`, e ele conta
 aviso — o INSERT que deu ER_DUP_ENTRY nunca chega aqui, porque lancou.)
o SELECT com esse id traz: {"id":1,"nm_aluno":"Ana","turma":3,"vl_nota":"8.50"}
e essa consulta, e nao o `affectedRows`, que e a prova de que gravou.

--- INSERT de tres linhas em um comando so ---
affectedRows: 3 <-- tres linhas, uma ida ao servidor
insertId:     2 <-- o id do PRIMEIRO inserted; o resto segue +1
o que esta na tabela:
  id 1 | Ana | turma 3
  id 2 | Bruno | turma 3
  id 3 | Carla | turma 4
  id 4 | Diego | turma 3
os ids continuam 1, 2, 3, 4 porque o TRUNCATE do comeco zerou o contador.

--- insertId com o id vindo pronto no INSERT ---
insertId: 99 <-- o id gravado, mesmo tendo vindo do chamador
affectedRows: 1 <-- e a linha foi gravada
  a intuição diz que `insertId` seria 0 ("o banco nao gerou nada");
  o banco devolve o id gravado. Por isso a prova continua sendo o SELECT.

--- INSERT IGNORE: a duplicata vira affectedRows 0 ---
affectedRows:  0 <-- zero: nada foi gravado
warningStatus: 1 <-- o banco avisou, e nao parou
o que ficou na linha 99: {"nm_aluno":"Semente"} <-- o primeiro, nao o novo

--- a mesma duplicata SEM IGNORE ---
  erro esperado: ER_DUP_ENTRY
  o `code` e o que o Node compara; a mensagem muda entre versoes.
  e o `sqlState` tambem: e o codigo padrao ANSI, o mesmo em qualquer SGBD.
  sqlState: 23000

--- ON DUPLICATE KEY UPDATE ---
affectedRows: 2 <-- 2 significa UPDATE, 1 significa INSERT
a linha 99 agora e: {"nm_aluno":"Atualizado","vl_nota":"7.75"}

o MESMO comando de novo, com o MESMO valor: affectedRows 1
  a intuicao diz 0 ("nao mudou nada") e o banco devolve 1. Medido, nao suposto.
  e no `UPDATE` quem distingue as duas coisas e o `info` e o `changedRows`:
  UPDATE com o mesmo valor -> affectedRows 1 | changedRows 0 | info: "Rows matched: 1  Changed: 0  Warnings: 0"
  UPDATE mudando o valor   -> affectedRows 1 | changedRows 1 | info: "Rows matched: 1  Changed: 1  Warnings: 0"

--- INSERT ... SELECT: de uma tabela para outra, sem passar pelo Node ---
copiou as notas >= 7.00 | affectedRows: 3
  Ana -> 8.50
  Atualizado -> 8.00
  Carla -> 9.25
o dado foi copiado DENTRO do banco: a rede entre Node e MySQL
transportou so a instrucao, nao as linhas.

--- TRUNCATE devolve affectedRows 0 ---
affectedRows: 0 <-- apagou a tabela inteira e devolve 0
  e por isso que a aula de DELETE diz: para contar o que sumiu, conta antes.

--- o que `query` devolve, por tipo de comando ---
SELECT    -> [linhas, campos]      linhas = array de objetos, campos = metadados
INSERT    -> [resultado, undefined] resultado = affectedRows, insertId, warningStatus
UPDATE    -> [resultado, undefined] resultado = affectedRows, changedRows, info
DELETE    -> [resultado, undefined] affectedRows = quantas sumiram
TRUNCATE  -> [resultado, undefined] affectedRows = 0, sempre

E por isso que a desestruturacao e sempre [algo, campos]: o primeiro valor
e a coisa que interessa, e o segundo so importa em SELECT, onde sao os
metadados das colunas.

conexao encerrada com end().
Aula 2

SELECT: ler dados

SELECT: ler dados

SELECT é o comando que devolve linhas. A consulta tem seis peças, e elas entram

nesta ordem: o que pegar, de onde, com que filtro, em que ordem,

quantas, a partir de qual.

SELECT nm_aluno, vl_nota      -- o que
  FROM tb_curso                -- de onde
 WHERE turma = 3               -- com que filtro
 ORDER BY vl_nota DESC         -- em que ordem
 LIMIT 10 OFFSET 0;            -- quantas, a partir de qual

As linhas chegam em Node como um array de objetos, com o nome da coluna como

chave. linhas[0].nm_aluno é a primeira linha, primeira coluna.

SELECT *: atalho em leitura, defeito em escrita

* traz todas as colunas, inclusive as que ninguém pediu. Em SELECT é atalho,

com custo de rede e memória — a coluna volta mesmo se o Node não usa. Em

UPDATE é o defeito da aula 13: UPDATE t SET * não existe como se quer, e quem

tenta escrever todas as colunas apaga o valor das que o WHERE não marca.

WHERE: o filtro, e a comparação de nulo

WHERE turma = 3
WHERE vl_nota >= 7.00
WHERE vl_nota IS NULL

IS NULL é a única forma de comparar com nulo. = NULL e != NULL

devolvem zero linha sempre, porque em SQL nulo não se compara com nulo — a

resposta é "desconhecido", e o banco trata desconhecido como falso. É o erro

clássico: o filtro parece correto e devolve nada.

ORDER BY sem ordem definida

Sem ORDER BY a ordem é indefinida: o banco devolve na ordem que for mais

rápida, e ela muda com o volume, com o índice e com a versão. `ORDER BY nm_aluno

ASC ordena crescente; DESC`, decrescente.

NULL vem sempre primeiro em ASC e último em DESC, e não é acaso: o

banco trata nulo como menor que qualquer valor, para poder ordenar por índice. Por

isso que "tirar os vazios do começo" se faz no WHERE, e não no ORDER BY.

LIMIT e OFFSET

LIMIT 3         -- as três primeiras
LIMIT 3 OFFSET 3   -- da quarta em diante

É assim que funciona paginação — e é exatamente por isso que ela degrada: em

página 500, o banco lê e descarta as 500 primeiras linhas. O limite é do

banco, não da máquina.

COUNT e as agregações

FunçãoO que conta
COUNT(*)linhas
COUNT(coluna)linhas onde a coluna não é nula
COUNT(DISTINCT col)valores distintos
AVG / SUM / MIN / MAXagregação numérica

COUNT(*) e COUNT(coluna) só diferem quando a coluna tem nulo. Com 6 linhas e 5

notas, o primeiro devolve 6 e o segundo 5.

AVG e SUM passam para aritmética de ponto flutuante mesmo em coluna DECIMAL,

e por isso não devem ser usados com dinheiro: o valor exato está nas linhas, a

agregação já é aproximada.

Contar linhas tem três formas, e elas só coincidem quando não há NULL:

SELECT COUNT(*) FROM tb_curso;             -- linhas
SELECT COUNT(vl_nota) FROM tb_curso;      -- linhas com nota preenchida
SELECT COUNT(DISTINCT turma) FROM tb_curso;  -- valores distintos

Quem precisa do total para a resposta de uma API quer o primeiro: o número que

o Node lê é linhas[0].n de um SELECT COUNT(*) AS n, e ele é number

porque COUNT é inteiro — ao contrário do SUM, que devolve string quando

soma coluna DECIMAL.

[linhas, campos]: o segundo valor

const [linhas, campos] = await conexao.query('SELECT id, vl_nota FROM ...');

linhas são os dados. campos são os metadados das colunas, com name,

columnType (o código numérico do protocolo) e type. É por aí que se descobre

o que o driver traduziu: a mesma coluna date chega como objeto Date por

padrão, e como texto YYYY-MM-DD quando a conexão liga dateStrings. E o

decimal chega como string, porque o tipo é exato — quem soma depois precisa

converter com Number(), senão concatena texto.

linhas é sempre um array, mesmo com zero resultados. Por isso o 404 de

uma rota de lista não vem de undefined: vem de linhas.length === 0.

Exemplo

'use strict';

// Exemplo da aula 2 do dia 9: `SELECT`, filtrar, ordenar, limitar e contar.
//
// `SELECT` e o comando que devolve linhas. A aula percorre as seis pecas na
// ordem em que elas aparecem no SQL: o que pegar, de onde, com que filtro, em que
// ordem, quantas, e a partir de qual. E no fim o `COUNT`, que e a unica consulta
// que o Node costuma precisar quando a API e "quantos tem".
//
// O ponto de risco da aula e `SELECT *`: ela traz todas as colunas, incluindo a
// que ninguem pediu, e o dia 13 mostra que `*` nao pode ir no `UPDATE`.

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(`
      CREATE TABLE IF NOT EXISTS tb_curso (
        id        INT AUTO_INCREMENT PRIMARY KEY,
        nm_aluno  VARCHAR(40)  NOT NULL,
        turma     INT          NOT NULL,
        vl_nota   DECIMAL(4,2) NULL,
        dt_cadastro DATE       NOT NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);
    // Base de teste FIXA: os ids ficam 1 a 6 em toda execucao, e e por isso que
    // a saida da pagina nao muda entre rodadas.
    await conexao.query('TRUNCATE TABLE tb_curso');
    await conexao.query(
      `INSERT INTO tb_curso (nm_aluno, turma, vl_nota, dt_cadastro) VALUES
       (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)`,
      [
        'Ana', 3, 8.50, '2026-05-04',
        'Bruno', 3, 6.00, '2026-05-04',
        'Carla', 4, 9.25, '2026-05-11',
        'Diego', 3, null, '2026-05-11',
        'Elis', 4, 7.00, '2026-05-18',
        'Fabio', 5, 4.50, '2026-05-18',
      ]
    );
    console.log('--- 6 alunos gravados (ids 1 a 6, toda execucao) ---');

    // --- 1. `SELECT`: as duas pecas obrigatorias ---
    //
    // `SELECT` diz O QUE, `FROM` diz DE ONDE. Sem `FROM` o banco nao tem onde ler;
    // sem `SELECT` nao ha o que devolver.
    const [todos] = await conexao.query('SELECT nm_aluno, turma FROM tb_curso');
    console.log('');
    console.log('--- SELECT nm_aluno, turma FROM tb_curso ---');
    for (const linha of todos) console.log('  ' + linha.nm_aluno + ' | turma ' + linha.turma);
    console.log('  as linhas chegam como um ARRAY DE OBJETOS, com o nome da coluna como chave.');
    console.log('  `nm_aluno` e a chave; o valor e o que o banco guardou.');

    // --- 2. `SELECT *` e o que ele traz a mais ---
    //
    // O `*` traz TODAS as colunas, incluindo as que ninguem pediu. Em leitura
    // ele e atalho; em `UPDATE` e perigo, e a aula 13 mostra o estrago. Em
    // `SELECT`, o custo e de rede e de memoria: a coluna volta mesmo se o Node
    // nao usa.
    const [estrelas] = await conexao.query('SELECT * FROM tb_curso WHERE id = ?', [1]);
    const [especificas] = await conexao.query('SELECT nm_aluno, turma FROM tb_curso WHERE id = ?', [1]);
    console.log('');
    console.log('--- SELECT * contra coluna especifica (mesma linha) ---');
    console.log('SELECT *              ->', JSON.stringify(estrelas[0]));
    console.log('SELECT nm_aluno, turma->', JSON.stringify(especificas[0]));
    console.log('  o `*` trouxe 3 colunas a mais: id, vl_nota e dt_cadastro.');
    console.log('  em `SELECT` isso e atalho. Em `UPDATE`, e o defeito da aula 13.');

    // --- 3. `WHERE`: o filtro ---
    //
    // O `WHERE` vem depois do `FROM` e e a condicao de manutencao. Sem ele, a
    // consulta traz a tabela inteira; com ele, traz o que interessa. E o lugar
    // onde o `?` entra: o valor NUNCA entra no texto do SQL.
    console.log('');
    console.log('--- WHERE: tres filtros ---');
    for (const [rotulo, sql, valor] of [
      ['turma = 3', 'SELECT nm_aluno, turma FROM tb_curso WHERE turma = ?', 3],
      ['nota >= 7.00', 'SELECT nm_aluno, vl_nota FROM tb_curso WHERE vl_nota >= ?', 7.0],
      ['nota IS NULL', 'SELECT nm_aluno, vl_nota FROM tb_curso WHERE vl_nota IS NULL', null],
    ]) {
      const [linhas] = await conexao.query(sql, valor === null ? [] : [valor]);
      console.log('  ' + rotulo.padEnd(14) + ' -> ' + linhas.length + ' linha(s): ' +
        (linhas.length ? linhas.map((l) => l.nm_aluno + (l.vl_nota !== undefined ? ' (' + l.vl_nota + ')' : '')).join(', ') : 'nenhuma'));
    }
    console.log('');
    console.log('`IS NULL` e a unica comparacao de nulo: `= NULL` e `!= NULL`');
    console.log('devolvem zero linha sempre, porque nulo nao se compara com nulo.');

    const [nuloErrado] = await conexao.query('SELECT COUNT(*) AS n FROM tb_curso WHERE vl_nota = NULL');
    const [nuloCerto] = await conexao.query('SELECT COUNT(*) AS n FROM tb_curso WHERE vl_nota IS NULL');
    console.log('  WHERE vl_nota = NULL   ->', nuloErrado[0].n, 'linha(s)  <- o erro classico');
    console.log('  WHERE vl_nota IS NULL  ->', nuloCerto[0].n, 'linha(s)  <- o certo');

    // --- 4. `ORDER BY` e `LIMIT` ---
    //
    // `ORDER BY` ordena, e sem `ORDER BY` a ordem é indefinida: o banco devolve na
    // ordem que for mais rápida, e ela muda com o volume e com o índice. E o
    // motivo de "a lista mudou de ordem sozinha" não ser bug do frontend.
    console.log('');
    console.log('--- ORDER BY e LIMIT ---');
    for (const [rotulo, sql] of [
      ['sem ORDER BY', 'SELECT id, nm_aluno FROM tb_curso LIMIT 3'],
      ['por nome ASC', 'SELECT id, nm_aluno FROM tb_curso ORDER BY nm_aluno ASC LIMIT 3'],
      ['por nome DESC', 'SELECT id, nm_aluno FROM tb_curso ORDER BY nm_aluno DESC LIMIT 3'],
      ['so quem tem nota', 'SELECT id, nm_aluno, vl_nota FROM tb_curso WHERE vl_nota IS NOT NULL ORDER BY vl_nota DESC LIMIT 3'],
    ]) {
      const [linhas] = await conexao.query(sql);
      console.log('  ' + rotulo.padEnd(16) + ' -> ' + linhas.map((l) => l.nm_aluno).join(', '));
    }
    console.log('');
    console.log('`NULL` vem sempre PRIMEIRO em `ASC` e ULTIMO em `DESC`, e nao e acaso:');
    console.log('o banco trata nulo como menor que qualquer valor, para poder ordenar por indice.');
    console.log('por isso que "filtrar os vazios" se faz no WHERE, e nao no ORDER BY.');

    // --- 5. `OFFSET`: pular as primeiras linhas ---
    //
    // `LIMIT 3 OFFSET 3` devolve da quarta linha em diante. E assim que funciona
    // paginacao — e e exatamente por isso que paginacao com `OFFSET` degrada: em
    // pagina 500, o banco le e descarta as 500 primeiras linhas.
    const [pagina1] = await conexao.query('SELECT nm_aluno FROM tb_curso ORDER BY id LIMIT 3 OFFSET 0');
    const [pagina2] = await conexao.query('SELECT nm_aluno FROM tb_curso ORDER BY id LIMIT 3 OFFSET 3');
    console.log('');
    console.log('--- LIMIT com OFFSET (paginacao) ---');
    console.log('pagina 1 (OFFSET 0):', pagina1.map((l) => l.nm_aluno).join(', '));
    console.log('pagina 2 (OFFSET 3):', pagina2.map((l) => l.nm_aluno).join(', '));
    console.log('  o `OFFSET` faz o banco ler e descartar as linhas puladas: em pagina');
    console.log('  alta o custo cresce, e o limite e o proprio banco, nao a maquina.');

    // --- 6. `COUNT` e as agregacoes ---
    //
    // `COUNT(*)` conta linhas; `COUNT(coluna)` conta as nao nulas. A diferenca
    // aparece em coluna com nulo, e e por isso que a distinção importa.
    const [totais] = await conexao.query(`
      SELECT COUNT(*) AS todas,
             COUNT(vl_nota) AS com_nota,
             COUNT(DISTINCT turma) AS turmas,
             AVG(vl_nota) AS media,
             MIN(vl_nota) AS menor,
             MAX(vl_nota) AS maior
        FROM tb_curso`);
    console.log('');
    console.log('--- agregacoes ---');
    const t = totais[0];
    console.log('COUNT(*)          todas as linhas            =', t.todas);
    console.log('COUNT(vl_nota)    linhas com nota            =', t.com_nota);
    console.log('COUNT(DISTINCT turma) turmas distintas        =', t.turmas);
    console.log('AVG(vl_nota)      media                      =', t.media);
    console.log('MIN / MAX         menor e maior nota         =', t.menor, '/', t.maior);
    console.log('');
    console.log('COUNT(*) e COUNT(coluna) so diferem quando a coluna tem nulo:');
    console.log('  6 linhas, mas so', t.com_nota, 'têm nota. O `COUNT(*)` conta a linha, nao o valor.');
    console.log('');
    console.log('`AVG` e `SUM` devolvem float e NAO devem ser usados com dinheiro:');
    console.log('DECIMAL e exato, mas a agregacao ja passou para aritmética de ponto flutuante.');

    // --- 7. o `[linhas, campos]`, medido ---
    //
    // O segundo valor do `query` sao os CAMPOS: os metadados das colunas. E ele
    // que diz o tipo que o driver traduziu, e e por ele que se descobre por que
    // uma `date` voltou como `Date` e um `decimal` voltou como string.
    const [linhas, campos] = await conexao.query(
      'SELECT id, nm_aluno, vl_nota, dt_cadastro FROM tb_curso WHERE id = ?', [1]
    );
    console.log('');
    console.log('--- o segundo valor do query: os campos ---');
    console.log('linhas[0]:', JSON.stringify(linhas[0]));
    console.log('campos:');
    // Cada `campo` traz o codigo numerico do tipo no protocolo (`columnType`) e
    // o nome do tipo segundo o SQL (`type`). Nenhum dos dois ja vem como `Date`
    // ou como string: quem traduz e o driver. E por isso que a mesma coluna
    // `date` pode chegar de dois jeitos, conforme a conexao: como objeto `Date`
    // por padrao, ou como texto `YYYY-MM-DD` quando o `dateStrings` esta ligado.
    for (const c of campos) {
      const valor = linhas[0][c.name];
      const emNode = valor === null ? 'null' : valor.constructor.name;
      console.log('  ' + String(c.name).padEnd(14) +
        ' columnType ' + String(c.columnType).padEnd(5) +
        ' chega em Node como: ' + emNode);
    }
    console.log('');
    console.log('  dt_cadastro (codigo 10,  date)    -> ' + linhas[0].dt_cadastro.constructor.name +
      ' ' + JSON.stringify(linhas[0].dt_cadastro.toISOString()));
    console.log('  vl_nota     (codigo 246, decimal) -> ' + typeof linhas[0].vl_nota +
      ' "' + linhas[0].vl_nota + '"');
    console.log('');
    console.log('`decimal` volta como texto porque o tipo e EXATO: quem soma depois');
    console.log('precisa converter com Number(), senao concatena string.');
    console.log('');
    console.log('os dois valores que `query` devolve:');
    console.log('  [0] = linhas  -> os dados');
    console.log('  [1] = campos  -> os metadados das colunas');
    console.log('destruir so com [linhas] funciona, porque o segundo e o que nao interessa');
    console.log('no `SELECT` mais comum. E por isso que escrever `const [versao] = await`');
    console.log('pega as LINHAS, e a primeira delas e `versao[0]`.');

    // --- 8. a forma como o Node le a linha ---
    console.log('');
    console.log('--- como a linha chega no Node ---');
    console.log('a linha e um objeto plano:');
    console.log('  typeof linha          =', typeof linhas[0]);
    console.log('  Object.keys(linha)    =', Object.keys(linhas[0]).join(', '));
    console.log('  linhas.length         =', linhas.length, '(0 quando nada foi encontrado, nunca undefined)');
    console.log('');
    console.log('`linhas` e SEMPRE um array, mesmo com 0 resultados. E por isso que o');
    console.log('`404` de uma rota de lista nao vem de `undefined`: ele vem de `length === 0`.');
  } 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

--- 6 alunos gravados (ids 1 a 6, toda execucao) ---

--- SELECT nm_aluno, turma FROM tb_curso ---
  Ana | turma 3
  Bruno | turma 3
  Carla | turma 4
  Diego | turma 3
  Elis | turma 4
  Fabio | turma 5
  as linhas chegam como um ARRAY DE OBJETOS, com o nome da coluna como chave.
  `nm_aluno` e a chave; o valor e o que o banco guardou.

--- SELECT * contra coluna especifica (mesma linha) ---
SELECT *              -> {"id":1,"nm_aluno":"Ana","turma":3,"vl_nota":"8.50","dt_cadastro":"2026-05-03T22:00:00.000Z"}
SELECT nm_aluno, turma-> {"nm_aluno":"Ana","turma":3}
  o `*` trouxe 3 colunas a mais: id, vl_nota e dt_cadastro.
  em `SELECT` isso e atalho. Em `UPDATE`, e o defeito da aula 13.

--- WHERE: tres filtros ---
  turma = 3      -> 3 linha(s): Ana, Bruno, Diego
  nota >= 7.00   -> 3 linha(s): Ana (8.50), Carla (9.25), Elis (7.00)
  nota IS NULL   -> 1 linha(s): Diego (null)

`IS NULL` e a unica comparacao de nulo: `= NULL` e `!= NULL`
devolvem zero linha sempre, porque nulo nao se compara com nulo.
  WHERE vl_nota = NULL   -> 0 linha(s)  <- o erro classico
  WHERE vl_nota IS NULL  -> 1 linha(s)  <- o certo

--- ORDER BY e LIMIT ---
  sem ORDER BY     -> Ana, Bruno, Carla
  por nome ASC     -> Ana, Bruno, Carla
  por nome DESC    -> Fabio, Elis, Diego
  so quem tem nota -> Carla, Ana, Elis

`NULL` vem sempre PRIMEIRO em `ASC` e ULTIMO em `DESC`, e nao e acaso:
o banco trata nulo como menor que qualquer valor, para poder ordenar por indice.
por isso que "filtrar os vazios" se faz no WHERE, e nao no ORDER BY.

--- LIMIT com OFFSET (paginacao) ---
pagina 1 (OFFSET 0): Ana, Bruno, Carla
pagina 2 (OFFSET 3): Diego, Elis, Fabio
  o `OFFSET` faz o banco ler e descartar as linhas puladas: em pagina
  alta o custo cresce, e o limite e o proprio banco, nao a maquina.

--- agregacoes ---
COUNT(*)          todas as linhas            = 6
COUNT(vl_nota)    linhas com nota            = 5
COUNT(DISTINCT turma) turmas distintas        = 3
AVG(vl_nota)      media                      = 7.050000
MIN / MAX         menor e maior nota         = 4.50 / 9.25

COUNT(*) e COUNT(coluna) so diferem quando a coluna tem nulo:
  6 linhas, mas so 5 têm nota. O `COUNT(*)` conta a linha, nao o valor.

`AVG` e `SUM` devolvem float e NAO devem ser usados com dinheiro:
DECIMAL e exato, mas a agregacao ja passou para aritmética de ponto flutuante.

--- o segundo valor do query: os campos ---
linhas[0]: {"id":1,"nm_aluno":"Ana","vl_nota":"8.50","dt_cadastro":"2026-05-03T22:00:00.000Z"}
campos:
  id             columnType 3     chega em Node como: Number
  nm_aluno       columnType 253   chega em Node como: String
  vl_nota        columnType 246   chega em Node como: String
  dt_cadastro    columnType 10    chega em Node como: Date

  dt_cadastro (codigo 10,  date)    -> Date "2026-05-03T22:00:00.000Z"
  vl_nota     (codigo 246, decimal) -> string "8.50"

`decimal` volta como texto porque o tipo e EXATO: quem soma depois
precisa converter com Number(), senao concatena string.

os dois valores que `query` devolve:
  [0] = linhas  -> os dados
  [1] = campos  -> os metadados das colunas
destruir so com [linhas] funciona, porque o segundo e o que nao interessa
no `SELECT` mais comum. E por isso que escrever `const [versao] = await`
pega as LINHAS, e a primeira delas e `versao[0]`.

--- como a linha chega no Node ---
a linha e um objeto plano:
  typeof linha          = object
  Object.keys(linha)    = id, nm_aluno, vl_nota, dt_cadastro
  linhas.length         = 1 (0 quando nada foi encontrado, nunca undefined)

`linhas` e SEMPRE um array, mesmo com 0 resultados. E por isso que o
`404` de uma rota de lista nao vem de `undefined`: ele vem de `length === 0`.

conexao encerrada com end().