Dia 9 — SQL: inserir e consultar
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
| Campo | O que é |
|---|---|
affectedRows | quantas linhas entraram |
insertId | o id gravado (o que o banco gerou, ou o que veio no INSERT) |
warningStatus | quantos avisos — não erros |
changedRows | quantas mudaram de valor |
info | o texto do servidor, como "Rows matched: 1 Changed: 1" |
fieldCount | quantas 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
| Comando | Primeiro valor | Segundo |
|---|---|---|
SELECT | [linhas, campos] — linhas são objetos | metadados das colunas |
INSERT | affectedRows, insertId, warningStatus | undefined |
UPDATE | affectedRows, changedRows, info | undefined |
DELETE | affectedRows | undefined |
TRUNCATE | affectedRows (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().
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ção | O que conta |
|---|---|
COUNT(*) | linhas |
COUNT(coluna) | linhas onde a coluna não é nula |
COUNT(DISTINCT col) | valores distintos |
AVG / SUM / MIN / MAX | agregaçã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().