MySQL: o banco que guarda — Arduino e IoT — semana 4 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 4 · MySQL: o banco que guarda — Material de Apoio Arduino

Semana 4 de 16· 2o trimestre · 01/05 a 04/09

MySQL: o banco que guarda

Schema, tipos, INSERT e SELECT. A primeira tabela que responde uma pergunta.

Aula 1 — Modelar a tabela de leitura e criar o schema

Objetivos

  • Modelar a tabela de leitura, dizendo para que serve cada coluna e que tipo ela precisa.
  • Criar a tabela com CREATE TABLE, incluindo chave primaria e índice, e ler de volta o schema criado.
  • Justificar por que DECIMAL e não FLOAT para temperatura, e o que muda na pratica.
  • Explicar a diferença entre dt_leitura e dt_gravacao, e por que as duas colunas precisam existir.
  • Apontar, no DESCRIBE, o que o banco fez com cada tipo que o aluno pediu.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 protoboard de 830 pontos por dupla
  • Computador com Node.js 20, MySQL ou MariaDB 10.11, e um cliente de banco
  • Arquivo .env com a credencial do banco de aula, com os nomes das variáveis na lousa
  • Folha de papel por dupla, para o desenho do schema
  • Projetor, para o terminal do professor

Conceitos

Uma linha por leitura, e não uma linha por dia

A decisao de modelagem mais importante do dia: a tabela guarda uma linha por leitura, com data e hora. Não uma linha por hora, não uma linha por media.

O motivo e a pergunta que o banco precisa responder no dia 6. Se você guarda a media por hora, você perde o pico da temperatura. Se você guarda cada leitura, você consegue calcular a media por hora na hora que quiser, e ainda mostra a minima e a maxima. Guardar o detalhe custa mais espaco e nunca custa informação; guardar a agregacao economiza espaco e para sempre.

As sete colunas

ColunaTipoPor que este tipo
idINT AUTO_INCREMENTchave primaria, o banco gera
id_placaVARCHAR(32)texto, porque tem letras
temp_cDECIMAL(5,2)até 999,99 com duas casas
umidade_pctDECIMAL(5,2)mesmo formato do percentual
dt_leituraDATETIMEquando a placa mediu
dt_gravacaoDATETIME DEFAULT CURRENT_TIMESTAMPquando o banco gravou
índiceINDEX (id_placa, dt_leitura)para o dia 6

DECIMAL contra FLOAT

FLOAT guarda o número em notacao cientifica binaria, e 26,5 não e exatamente representavel nessa base: o banco guarda o valor mais proximo, e a leitura volta com uma diferença na terceira ou quarta casa.

DECIMAL(5,2) guarda o número como decimal, com duas casas exatas. O que entra e o que sai, sempre. Para temperatura, umidade, e qualquer medida fisica, DECIMAL e a escolha certa. O professor mostra os dois lado a lado no terminal, e o aluno ve a diferença em 0.01.

dt_leitura contra dt_gravacao

São dois instantes diferentes e o banco precisa dos dois:

  • dt_leitura e o momento em que o sensor mediu, e vem dentro do JSON, enviado pela placa.
  • dt_gravacao e o momento em que o INSERT aconteceu, e e preenchido pelo proprio banco.

Quando a placa esta lenta, chega tarde ou a fila cresce, os dois valores se separam. No dia 7 o professor mede essa separacao com TIMESTAMPDIFF e mostra que a diferença entre os dois e o atraso do caminho. Se so existisse uma coluna de hora, essa medida seria impossivel.

Atividade

Montagem: nenhuma. A placa fica com o circuito do dia 1 e mostra a leitura, para o aluno ter de onde vem o valor que entra na tabela.

  1. Desenhe no papel a tabela com sete colunas. Para cada uma, escreva o tipo e uma frase dizendo para que serve.
  2. Escreva o CREATE TABLE completo, com chave primaria e índice. Nomeie a tabela tb_leitura e as colunas em portugues com sublinhado.
  3. Crie a tabela no banco de aula e rode o DESCRIBE.
  4. Comparar, lado a lado, o que você pediu e o que o banco devolveu para cada coluna. Escreva duas linhas de diferença que você encontrou.
  5. Troque DECIMAL(5,2) por FLOAT em uma tabela de teste, insira 26,5 e le de volta. O que mudou? Escreva o número exato que voltou.
  6. Explique em uma frase: se a tabela guardasse so a media por hora, o que o dia 6 não seria capaz de mostrar?
  7. Rode SHOW CREATE TABLE tb_leitura e guarde a saída no caderno. Essa e a definição real da tabela, e não o que você digitou.

Nota: 11 pontos. Critério de fim: a tabela existe no banco, o DESCRIBE esta anotado e os itens 4 e 5 tem as diferancas escritas com números.

Resolucao

O exemplo que cria o schema e le de volta, executado nesta maquina:

// Criar o schema da tabela de leitura. A tabela e a unica coisa que
// sobrevive ao fim do processo Node -- e por isso que ela precisa de nome.
const mysql = require('mysql2/promise');

(async () => {
  const pool = mysql.createPool({
    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,
  });

  await pool.query(
    CREATE TABLE IF NOT EXISTS tb_leitura (
      id           INT AUTO_INCREMENT,
      id_placa     VARCHAR(32)  NOT NULL,
      temp_c       DECIMAL(5,2) NOT NULL,
      umidade_pct  DECIMAL(5,2) NOT NULL,
      dt_leitura   DATETIME     NOT NULL,
      dt_gravacao  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (id),
      INDEX ix_leitura_placa_data (id_placa, dt_leitura)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  );
  console.log('tb_leitura criada ou ja existia');

  // DESCRIBE e o "print" do schema: devolve as colunas como o banco as guardou.
  const [colunas] = await pool.query('DESCRIBE tb_leitura');
  for (const c of colunas) {
    console.log(' ', c.Field.padEnd(12), c.Type.padEnd(12), c.Null === 'NO' ? 'obrigatorio' : 'opcional', c.Key || '');
  }
  await pool.end();
})();

A saída real do DESCRIBE, medida nesta maquina contra o MariaDB 10.11:

tb_leitura criada ou ja existia
  id           int(11)      obrigatorio PRI
  id_placa     varchar(32)  obrigatorio MUL
  temp_c       decimal(5,2) obrigatorio
  umidade_pct  decimal(5,2) obrigatorio
  dt_leitura   datetime     obrigatorio
  dt_gravacao  datetime     obrigatorio

O professor aponta três coisas nessa saída: o MUL na id_placa e o índice funcionando, o PRI na id e a chave primaria, e o decimal(5,2) minusculo, porque o banco normaliza tudo para minusculo. O aluno que escreveu TEMP_C descobre aqui que o banco não fez distincao, e e por isso que o padrao do curso e minusculo com sublinhado.

O IF NOT EXISTS e o que permite rodar o exemplo duas vezes sem erro, e o DEFAULT CURRENT_TIMESTAMP e o que dispensa o dt_gravacao no INSERT do dia 4 aula 2: o banco poe a hora sozinho.

O script todo esta em crase, e não em aspas simples. SQL com varias linhas não cabe em uma string de aspas simples em JavaScript: a aspa simples fecha no primeiro espaço+quebra, e o Node lanca SyntaxError: Invalid or unexpected token. O aluno vai encontrar esse erro sozinho quando tentar.

O sketch da placa mostra o lado de dentro do schema: cada campo do registro tem um nome e um tipo, e e a correspondencia entre esses nomes e as colunas do banco que o INSERT do Node vai depender.

// Aula 1 do dia 4: o dado existe, e precisa de endereco.
//
// Do lado do Node, a tabela tb_leitura tem sete colunas. Esta placa nao tem
// como ver essa tabela, mas ela tem o mesmo trabalho a fazer: transformar um
// numero solto em um registro com nome, tipo e espaco reservado. As duas
// colunas do JSON batem com as colunas do banco, e essa correspondencia e
// o tema do dia inteiro.

#include <Arduino.h>

const int PIN_SENSOR_ANALOGICO = 34;

// As mesmas colunas do schema, na mesma ordem do INSERT do Node.
const int TOTAL_DE_COLUNAS = 5;
const char* NOMES[] = {
  "id_placa",     // VARCHAR(32)  - quem mandou
  "temp_c",       // DECIMAL(5,2) - quanto valia
  "umidade_pct",  // DECIMAL(5,2) - quanto valia
  "fuso",         // INT          - deslocamento do relogio
  "dt_leitura"    // DATETIME     - quando a placa mediu
};

// Larguras de coluna na saida do monitor. ASCII nao tem caractere de
// alinhamento automatico, entao o alinhamento e um array.
const int LARGURAS[] = {12, 10, 14, 6, 21};

const int RESOLUCAO_ADC = 4095;
const int FUSO_HORAS = -3;

void imprimir_cabecalho() {
  Serial.println("registro que a placa vai mandar, campo por campo:");
  Serial.println();
  for (int i = 0; i < TOTAL_DE_COLUNAS; i++) {
    Serial.print(NOMES[i]);
    for (int e = strlen(NOMES[i]); e < LARGURAS[i]; e++) {
      Serial.print(" ");
    }
  }
  Serial.println();
}

// String nao pode ser declarado como vetor de referencias: isso nao
// compila em ESP32. O vetor de String e o jeito certo de passar uma
// lista de texto para uma funcao.
void imprimir_linha(String campos[], int quantos) {
  for (int i = 0; i < quantos; i++) {
    Serial.print(campos[i]);
    for (int e = strlen(campos[i].c_str()); e < LARGURAS[i]; e++) {
      Serial.print(" ");
    }
  }
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println();
  Serial.println("2o trimestre, dia 4, aula 1 - cada campo tem tipo");
  Serial.println("=================================================");
  Serial.println("do lado do Node, o schema e:");
  Serial.println("  id, id_placa, temp_c, umidade_pct, dt_leitura, dt_gravacao");
  Serial.println();
  Serial.println("a placa so precisa preencher cinco deles. O resto e do banco.");
  Serial.println();
  imprimir_cabecalho();
}

void loop() {
  int bruto = analogRead(PIN_SENSOR_ANALOGICO);
  float temp_c = (bruto / 4095.0f) * 50.0f;

  // Mesmo formato do JSON do dia 3: chave, dois pontos, valor.
  String campos[TOTAL_DE_COLUNAS];
  campos[0] = "esp32-lab01";
  campos[1] = String(temp_c, 1);
  campos[2] = String(55 + (bruto % 10));
  campos[3] = String(FUSO_HORAS);
  campos[4] = "2026-09-30 10:00:00";

  imprimir_linha(campos, TOTAL_DE_COLUNAS);
  Serial.println();
  Serial.println("cinco campos, cinco tipos no banco. Nome que nao bate,");
  Serial.println("o INSERT do Node quebra — e o erro do dia 5.");
  delay(2000);
}

Criterios de correcao

CritérioPontos
Sete colunas desenhadas, com tipo e função3 pontos
CREATE TABLE com chave primaria e índice, tudo em minusculo2 pontos
DESCRIBE executado e anotado1 ponto
Item 4: duas diferenças entre o pedido e o devolvido, com o que o banco fez2 pontos
Item 5: o número devolvido pelo FLOAT esta escrito2 pontos
Item 6: o que a media por hora faria perder esta em uma frase1 ponto

Erros comuns

ErroComo apareceCorrecao
Criar a tabela com o CREATE TABLE em aspas simples e varias linhasSyntaxError: Invalid or unexpected token"SQL de varias linhas vai entre crases, em JavaScript. As aspas simples fecham no primeiro espaco seguido de quebra de linha."
Nome de coluna em portugues com maiuscula, tipo Temperatura"Usei o mesmo nome do desenho""Minusculo e sublinhado: temp_c. O banco normaliza para minusculo, e quando o INSERT do dia 4 aula 2 não bater, o erro e nome de coluna."
Usar FLOAT porque "todo mundo usa"Item 5 sem diferença observada"FLOAT guarda em notacao binaria e 26,5 não existe exatamente nessa base. DECIMAL(5,2) guarda o decimal. Para medida fisica, DECIMAL sempre."
Criar so uma coluna de hora, dt_horaSchema com seis colunas em vez de sete"São dois instantes: quando a placa mediu e quando o banco gravou. Com uma coluna so, o dia 7 não consegue medir o atraso."
Esquecer o índice e descobrir so no dia 6Consulta lenta na aula 6"O índice não muda o resultado, muda o tempo. O dia 6 faz consulta por placa e por intervalo, e são exatamente as duas colunas do índice."
Usar VARCHAR para temperaturatemp_c como texto"Número em texto não soma, não tem media e ocupa mais espaco. DECIMAL guarda número e aceita calculo."
Deixar a tabela sem PRIMARY KEY"Criei a tabela, mas não tem id""Sem chave primaria não ha AUTO_INCREMENT e não ha linha unica. Toda tabela de leitura precisa de id."
Apagar a tabela toda a cada aulaDROP TABLE no comeco do script"Não use DROP. Use CREATE TABLE IF NOT EXISTS, e quando precisar zerar, use TRUNCATE. DROP apaga a estrutura, e o dia 5 depende dela."

Desafio extra

Crie uma segunda tabela, tb_leitura_hora, com uma linha por hora e por placa, com id_placa, dt_hora, temp_media, temp_min e temp_max. Não insira nada nela: apenas a estrutura. Depois escreva em uma frase o que essa tabela resolve e o que ela custa. A resposta que o professor espera: ela responde "como foi a tarde de hoje" em uma consulta rapida, e custa o trabalho de manter a agregacao atualizada a cada leitura — trabalho que o dia 6 mostra que o banco faz sozinho com GROUP BY, e que e o motivo de você provavelmente não precisar dela.

>

A resolucao, compilada

// Aula 1 do dia 4: o dado existe, e precisa de endereco.
//
// Do lado do Node, a tabela tb_leitura tem sete colunas. Esta placa nao tem
// como ver essa tabela, mas ela tem o mesmo trabalho a fazer: transformar um
// numero solto em um registro com nome, tipo e espaco reservado. As duas
// colunas do JSON batem com as colunas do banco, e essa correspondencia e
// o tema do dia inteiro.

#include <Arduino.h>

const int PIN_SENSOR_ANALOGICO = 34;

// As mesmas colunas do schema, na mesma ordem do INSERT do Node.
const int TOTAL_DE_COLUNAS = 5;
const char* NOMES[] = {
  "id_placa",     // VARCHAR(32)  - quem mandou
  "temp_c",       // DECIMAL(5,2) - quanto valia
  "umidade_pct",  // DECIMAL(5,2) - quanto valia
  "fuso",         // INT          - deslocamento do relogio
  "dt_leitura"    // DATETIME     - quando a placa mediu
};

// Larguras de coluna na saida do monitor. ASCII nao tem caractere de
// alinhamento automatico, entao o alinhamento e um array.
const int LARGURAS[] = {12, 10, 14, 6, 21};

const int RESOLUCAO_ADC = 4095;
const int FUSO_HORAS = -3;

void imprimir_cabecalho() {
  Serial.println("registro que a placa vai mandar, campo por campo:");
  Serial.println();
  for (int i = 0; i < TOTAL_DE_COLUNAS; i++) {
    Serial.print(NOMES[i]);
    for (int e = strlen(NOMES[i]); e < LARGURAS[i]; e++) {
      Serial.print(" ");
    }
  }
  Serial.println();
}

// String nao pode ser declarado como vetor de referencias: isso nao
// compila em ESP32. O vetor de String e o jeito certo de passar uma
// lista de texto para uma funcao.
void imprimir_linha(String campos[], int quantos) {
  for (int i = 0; i < quantos; i++) {
    Serial.print(campos[i]);
    for (int e = strlen(campos[i].c_str()); e < LARGURAS[i]; e++) {
      Serial.print(" ");
    }
  }
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println();
  Serial.println("2o trimestre, dia 4, aula 1 - cada campo tem tipo");
  Serial.println("=================================================");
  Serial.println("do lado do Node, o schema e:");
  Serial.println("  id, id_placa, temp_c, umidade_pct, dt_leitura, dt_gravacao");
  Serial.println();
  Serial.println("a placa so precisa preencher cinco deles. O resto e do banco.");
  Serial.println();
  imprimir_cabecalho();
}

void loop() {
  int bruto = analogRead(PIN_SENSOR_ANALOGICO);
  float temp_c = (bruto / 4095.0f) * 50.0f;

  // Mesmo formato do JSON do dia 3: chave, dois pontos, valor.
  String campos[TOTAL_DE_COLUNAS];
  campos[0] = "esp32-lab01";
  campos[1] = String(temp_c, 1);
  campos[2] = String(55 + (bruto % 10));
  campos[3] = String(FUSO_HORAS);
  campos[4] = "2026-09-30 10:00:00";

  imprimir_linha(campos, TOTAL_DE_COLUNAS);
  Serial.println();
  Serial.println("cinco campos, cinco tipos no banco. Nome que nao bate,");
  Serial.println("o INSERT do Node quebra — e o erro do dia 5.");
  delay(2000);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia04 aula1.

Aula 2 — Inserir e consultar: INSERT e SELECT na pratica

Objetivos

  • Inserir uma leitura com INSERT e parametro, e ler o insertId que o banco devolve.
  • Consultar com SELECT, filtrando com WHERE e ordenando com ORDER BY e LIMIT.
  • Passar valor por parametro ? em vez de concatenar, e dizer por que isso e obrigatorio.
  • Ler a diferença entre execute e query no mysql2 e escolher a certa em cada caso.
  • Reproduzir, e explicar, o deslocamento de fuso que o Node faz com a coluna DATETIME.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 protoboard de 830 pontos por dupla
  • Computador com Node.js 20 e MySQL ou MariaDB 10.11
  • Arquivo .env com a credencial do banco de aula
  • Folha de papel por dupla, para anotar as consultas e o que elas devolveram
  • Projetor, para o terminal do professor

Conceitos

INSERT e o insertId

INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura)
VALUES (?, ?, ?, ?)

Quatro ?, quatro valores, na mesma ordem. O banco devolve um objeto com insertId (o id que ele gerou) e affectedRows (quantas linhas mudaram).

O dt_gravacao não aparece no INSERT de propósito: ele tem DEFAULT CURRENT_TIMESTAMP, e o banco poe a hora. Se você mandar dt_gravacao do Node, a hora gravada passa a ser a hora do servidor Node, e não a hora do banco — o que parece a mesma coisa e não e, e volta no dia 11 quando o Node roda em outra maquina, em outro fuso.

Parametro contra concatenacao

// ERRADO: a string do SQL muda de forma
const sql = "SELECT * FROM tb_leitura WHERE id_placa = '" + placa + "'";

// CERTO: o SQL tem forma fixa, o valor entra separado
const [linhas] = await pool.execute(sql, [placa]);

O segundo trecho não e "mais seguro" por estilo: e que o SQL não muda de forma. O banco le a frase, decide o plano de execução, e substitui o ponto de interrogacao pelo valor. A concatenacao muda a frase, e o banco tem que decidir de novo — e qualquer aspa que o aluno coloque no valor fecha a string SQL e deixa o aluno executar um comando que não era para executar.

O curso de JavaScript do 1o trimestre já mostrou isso no innerHTML do document. Aqui o nome e outro (SQL injection, dia 9), e o resultado e o mesmo.

execute contra query

MetodoQuando usarDevolve
pool.execute(sql, [params])com parametro, o dia inteiroprepared statement, mais rapido
pool.query(sql)sem parametro: CREATE, DESCRIBE, SHOWsem prepared statement

O aluno que usar execute sem lista de parametros recebe o mesmo resultado, e não entende por que a aula insiste em query para o CREATE. O motivo e desempenho: sem parametro não ha o que preparar, e preparar uma frase sem parametro e trabalho jogado fora.

O fuso: o erro que o Node causa

Este e o item mais importante do dia, e so aparece quando o aluno usa DATETIME:

Com mysql2 no padrao, DATETIME volta como um objeto Date em JavaScript. O Date guarda o instante em UTC, e o MySQL guardou as 9 h locais. O resultado e que a hora volta deslocada, e o aluno ve 09:00 virar 07:00Z.

A correcao esta na configuração do pool:

mysql.createPool({ /* ... */ dateStrings: true });

Com dateStrings: true, o banco devolve o texto 2026-09-30 09:00:00, sem conversao nenhuma, e o que foi guardado e o que aparece. Para um sistema que so grava e relé, e a escolha certa.

Atividade

Montagem: nenhuma. A placa fica com o circuito do dia 1.

  1. Escreva o INSERT com quatro ? e a lista de valores na ordem certa. Confira: qual ? e o dt_leitura?
  2. Insira três leituras da sua placa e duas de outra placa ficticia. Anote o insertId de cada uma. Os ids pulam algum número? Por que?
  3. Consulte todas as linhas com ORDER BY id. Depois consulte so as da sua placa com WHERE. As duas consultas tem resultados diferentes? Quantas linhas cada uma devolveu?
  4. Troque ORDER BY id por ORDER BY dt_leitura DESC e repita. O que mudou na ordem? Com ORDER BY id DESC, as leituras aparecem na ordem em que chegaram ou na ordem em que aconteceram? Defina a diferença em uma frase.
  5. Adicione LIMIT 2 e explique em uma frase o que ele faz. Ele limita a quantidade de linhas ou o intervalo de tempo?
  6. Inserir 26,5 com virgula e ler de volta. O que aconteceu com o número? Tente 26.5 com ponto e le de volta. Qual das duas formas funciona?
  7. Adicione dateStrings: true no pool e rode a consulta de novo. A hora mudou? Escreva a hora que aparece antes e depois da mudanca.

Nota: 12 pontos. Critério de fim: existem pelo menos cinco linhas, com duas placas diferentes, e os itens 4, 5 e 7 tem resposta escrita com os números observados.

Resolucao

O exemplo foi executado duas vezes seguidas nesta maquina, e as duas execuções deram exatamente a mesma saída — e o que prova que o TRUNCATE no comeco cumpre o papel:

// INSERT e SELECT na pratica. O exemplo e idempotente: TRUNCATE no comeco,
// entao rodar duas vezes da o mesmo resultado.
const mysql = require('mysql2/promise');

(async () => {
  const pool = mysql.createPool({
    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,
    dateStrings: true,   // devolve DATETIME como texto, sem deslocar o fuso
  });

  await pool.query(CREATE TABLE IF NOT EXISTS tb_leitura (
    id INT AUTO_INCREMENT, id_placa VARCHAR(32) NOT NULL,
    temp_c DECIMAL(5,2) NOT NULL, umidade_pct DECIMAL(5,2) NOT NULL,
    dt_leitura DATETIME NOT NULL, dt_gravacao DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id), INDEX ix_leitura_placa_data (id_placa, dt_leitura)
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4);

  await pool.query('TRUNCATE TABLE tb_leitura');

  // INSERT de tres leituras, com o ? preenchido pelo array de valores.
  // O primeiro ? e id_placa, o segundo temp_c, o terceiro umidade_pct.
  const sql = 'INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura) VALUES (?, ?, ?, ?)';
  const linhas = [
    ['esp32-lab01', 27.40, 58, '2026-09-30 09:00:00'],
    ['esp32-lab01', 29.10, 55, '2026-09-30 09:05:00'],
    ['esp32-lab02', 22.80, 71, '2026-09-30 09:10:00'],
  ];
  for (const linha of linhas) {
    const [r] = await pool.execute(sql, linha);
    console.log('id gerado', r.insertId, '->', linha[1] + ' C');
  }

  const [todas] = await pool.execute(
    'SELECT id, id_placa, temp_c, umidade_pct, dt_leitura FROM tb_leitura ORDER BY id');
  console.log('--- todas as linhas, em ordem de chegada ---');
  for (const t of todas) {
    console.log(' ', t.id, t.id_placa, t.temp_c + ' C', t.umidade_pct + ' %', t.dt_leitura.slice(11, 19));
  }

  // WHERE com parametro: nunca concatenar o valor na string.
  const [uma] = await pool.execute(
    'SELECT COUNT(*) AS total FROM tb_leitura WHERE id_placa = ?', ['esp32-lab01']);
  console.log('leituras de esp32-lab01:', uma[0].total);

  const [limite] = await pool.execute(
    'SELECT * FROM tb_leitura ORDER BY id DESC LIMIT ?', [1]);
  console.log('ultima leitura inserida:', limite[0].id, limite[0].temp_c + ' C');

  await pool.end();
})();

A saída real, nas duas execuções:

id gerado 1 -> 27.4 C
id gerado 2 -> 29.1 C
id gerado 3 -> 22.8 C
--- todas as linhas, em ordem de chegada ---
  1 esp32-lab01 27.40 C 58.00 % 09:00:00
  2 esp32-lab01 29.10 C 55.00 % 09:05:00
  3 esp32-lab02 22.80 C 71.00 % 09:10:00
leituras de esp32-lab01: 2
ultima leitura inserida: 3 22.80 C

O professor aponta duas coisas nessa saída. A primeira: a coluna dt_leitura mostra 09:10:00, que e exatamente o que foi inserido — e so aparece isso porque o pool foi criado com dateStrings: true. A segunda: a coluna dt_gravacao não aparece no SELECT, mas existe na linha, preenchida pelo banco no momento do INSERT, com o horario da maquina onde o Node roda. Os dois instantes estão ali, diferentes, e o dia 7 mede o quanto diferentes com TIMESTAMPDIFF.

Note também que temp_c volta como 27.40 e não 27.4: e o DECIMAL(5,2) devolvendo as duas casas que você pediu. O aluno que esperava 27.4 esta vendo o tipo funcionando.

O sketch da placa mostra INSERT e SELECT sem MySQL, num vetor de memória, para o aluno ver as duas operacoes antes de ver a versão com banco:

// Aula 2 do dia 4: INSERT e SELECT do lado da placa.
//
// Do lado do Node, o INSERT escreve na tb_leitura e o SELECT le de volta.
// Esta placa mostra as duas operacoes no mesmo formato, sem MySQL: e a
// mesma ideia de "guardar e depois buscar" que o dia 4 executa no banco.

#include <Arduino.h>

const int PIN_SENSOR_ANALOGICO = 34;
const int TOTAL_DE_LEITURAS = 5;
const int RESOLUCAO_ADC = 4095;

// Estrutura de uma linha da tabela. E a mesma do banco, em C++: um tipo
// para o id, um tipo para cada medida e um texto para a data.
struct Leitura {
  int         id;
  char        id_placa[10];
  float       temp_c;
  int         umidade_pct;
  char        dt_leitura[20];
};

// O "banco" da placa: um vetor na memoria. Some quando a energia acaba,
// e e exatamente por isso que o MySQL existe.
Leitura tb_leitura[TOTAL_DE_LEITURAS];
int total_gravado = 0;
int proximo_id = 1;

// INSERT: acrescenta uma linha e devolve o id que o banco gerou.
int inserir(const char* placa, float temp_c, int umidade_pct, const char* quando) {
  if (total_gravado >= TOTAL_DE_LEITURAS) {
    return -1;   // tabela cheia: e o mesmo -1 que a aula 2 do dia 3 viu
  }
  Leitura& nova = tb_leitura[total_gravado];
  nova.id = proximo_id++;
  strncpy(nova.id_placa, placa, sizeof(nova.id_placa) - 1);
  nova.id_placa[sizeof(nova.id_placa) - 1] = '\0';   // o \0 e obrigatorio
  nova.temp_c = temp_c;
  nova.umidade_pct = umidade_pct;
  strncpy(nova.dt_leitura, quando, sizeof(nova.dt_leitura) - 1);
  nova.dt_leitura[sizeof(nova.dt_leitura) - 1] = '\0';
  return nova.id;
}

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println();
  Serial.println("2o trimestre, dia 4, aula 2 - INSERT e SELECT");
  Serial.println("=================================================");
  Serial.println("o vetor abaixo e a tb_leitura: 5 linhas, como o banco.");
  Serial.println();
  Serial.println("id | placa     | temp_c | umid | dt_leitura");
  Serial.println("---+-----------+--------+------+-----------------------------");
}

void loop() {
  if (total_gravado < TOTAL_DE_LEITURAS) {
    int bruto = analogRead(PIN_SENSOR_ANALOGICO);
    float temp_c = (bruto / 4095.0f) * 50.0f;
    int umidade_pct = 55 + (total_gravado % 6);

    char quando[20];
    snprintf(quando, sizeof(quando), "2026-09-30 10:0%d:00", total_gravado);

    // INSERT
    int id = inserir("esp32-lab01", temp_c, umidade_pct, quando);
    if (id < 0) {
      Serial.println("INSERT recusado: a tabela esta cheia (5 linhas).");
      Serial.println("no dia 7 a tabela cresce ate 172800 linhas por dia.");
    }
  }

  // SELECT: imprime a tabela inteira, do id mais baixo para o mais alto.
  // ORDER BY id e essa ordem. No dia 7 a ordem muda, e muda de proposito.
  for (int i = 0; i < total_gravado; i++) {
    Serial.print("   ");
    Serial.print(tb_leitura[i].id);
    Serial.print(" | ");
    Serial.print(tb_leitura[i].id_placa);
    for (int e = strlen(tb_leitura[i].id_placa); e < 9; e++) Serial.print(" ");
    Serial.print(" | ");
    Serial.print(tb_leitura[i].temp_c, 1);
    for (int e = 1; e < 6; e++) Serial.print(" ");
    Serial.print(" | ");
    Serial.print(tb_leitura[i].umidade_pct);
    for (int e = 1; e < 4; e++) Serial.print(" ");
    Serial.print(" | ");
    Serial.println(tb_leitura[i].dt_leitura);
  }

  if (total_gravado >= TOTAL_DE_LEITURAS) {
    Serial.println("a tabela esta cheia. DELETE o dia 7, aqui seria so");
    Serial.println("reduzir TOTAL_DE_LEITURAS para zero e recarregar a placa.");
  } else {
    Serial.println("...");
  }
  delay(1500);
}

Por que assim e não de outro jeito. O strncpy não garante terminador quando o texto e do tamanho exato do vetor. Este código resolve isso com sizeof(campo) - 1 no terceiro argumento: o strncpy copia um caractere a menos que o tamanho do vetor e deixa a ultima posicao livre. Ha outra forma comum, escrever o '\x00' na ultima posicao depois da copia, e as duas funcionam — mas não as duas ao mesmo tempo, e o aluno que mistura as duas recebe um aviso do compilador. Sem terminador, o proximo Serial.print procura o fim da string e le a memória seguinte, e o monitor mostra lixo. O professor já mostrou esse bug no dia 2 do 1o trimestre, e ele volta porque e o mesmo código.

O TOTAL_DE_LEITURAS = 5 existe para o aluno ver a tabela cheia e receber o -1. Sem limite, o vetor estouraria e a placa reiniciaria sozinha — que e o pior sintoma possivel, porque parece que o WiFi caiu. O dia 7 volta a esse -1 com a fila de tamanho fixo.

Criterios de correcao

CritérioPontos
INSERT com quatro ? e a ordem dos valores conferida2 pontos
Pelo menos cinco linhas inseridas, com duas placas diferentes, insertId anotado3 pontos
WHERE filtrando por placa e LIMIT limitando quantidade2 pontos
Item 4: a diferença entre ordem de chegada e ordem de acontecimento esta escrita2 pontos
Item 6: a forma que funciona esta identificada, com o número observado2 pontos
dateStrings: true no pool, com a hora antes e depois anotada1 ponto

Erros comuns

ErroComo apareceCorrecao
Concatenar o valor no SQLSELECT * FROM tb_leitura WHERE id_placa = ' + placa"O ? existe para isso. Troque por WHERE id_placa = ? e passe a placa no array. E o dia 9 trata por que isso e seguranca, não estilo."
Usar virgula no INSERT"O banco leu 26 e 5 como dois campos""Em SQL o decimal e ponto. A virgula e separador de coluna."
Não usar dateStrings e estranhar a horaLeitura inserida as 9 h, consulta devolve 7 h"O mysql2 converte DATETIME em Date, e Date e UTC. Ponha dateStrings: true no pool e a hora volta como foi guardada."
Esquecer o TRUNCATE e rodar duas vezes"insertId comecou em 7, não em 1""O AUTO_INCREMENT não anda para tras. Use TRUNCATE no comeco, ou DELETE com filtro, para o exemplo ser repetivel."
Mandar dt_gravacao no INSERTColuna duplicada ou valor diferente do esperado"Ela tem DEFAULT CURRENT_TIMESTAMP. Se você mandar, perde a hora do banco e passa a gravar a hora do Node. Deixe o banco cuidar."
Achar que ORDER BY id e ordem de tempoItem 4 com resposta trocada"id cresce na ordem da chegada. Se uma leitura antiga chegar depois, ela fica com id grande e dt_leitura pequena. O dia 7 mostra exatamente esse caso."
Usar LIMIT 5 esperando 5 leituras do intervalo"Pedeu 5 e veio leitura de ontem""LIMIT limita quantidade, não tempo. Para tempo, e o dia 6, com BETWEEN."
SELECT * e reclamar das colunas extras"Veio id e dt_gravacao e eu não pedi""* e "todas". Nomeie as colunas que você usa: SELECT id, temp_c FROM .... Além de ficar claro, economiza trafego."

Desafio extra

Escreva uma consulta que devolva a ultima leitura de cada placa, e não apenas a ultima leitura do banco. Depois explique em uma frase por que ORDER BY id DESC LIMIT 1 não faz isso. A resposta que o professor espera: ele devolve a ultima leitura de qualquer placa, e para ter uma por placa o filtro precisa vir antes do LIMIT — ou a consulta precisa de GROUP BY id_placa com MAX(id), que e exatamente a mesma combinacao do dia 6.

>

A resolucao, compilada

// Aula 2 do dia 4: INSERT e SELECT do lado da placa.
//
// Do lado do Node, o INSERT escreve na tb_leitura e o SELECT le de volta.
// Esta placa mostra as duas operacoes no mesmo formato, sem MySQL: e a
// mesma ideia de "guardar e depois buscar" que o dia 4 executa no banco.

#include <Arduino.h>

const int PIN_SENSOR_ANALOGICO = 34;
const int TOTAL_DE_LEITURAS = 5;
const int RESOLUCAO_ADC = 4095;

// Estrutura de uma linha da tabela. E a mesma do banco, em C++: um tipo
// para o id, um tipo para cada medida e um texto para a data.
struct Leitura {
  int         id;
  char        id_placa[10];
  float       temp_c;
  int         umidade_pct;
  char        dt_leitura[20];
};

// O "banco" da placa: um vetor na memoria. Some quando a energia acaba,
// e e exatamente por isso que o MySQL existe.
Leitura tb_leitura[TOTAL_DE_LEITURAS];
int total_gravado = 0;
int proximo_id = 1;

// INSERT: acrescenta uma linha e devolve o id que o banco gerou.
int inserir(const char* placa, float temp_c, int umidade_pct, const char* quando) {
  if (total_gravado >= TOTAL_DE_LEITURAS) {
    return -1;   // tabela cheia: e o mesmo -1 que a aula 2 do dia 3 viu
  }
  Leitura& nova = tb_leitura[total_gravado];
  nova.id = proximo_id++;
  strncpy(nova.id_placa, placa, sizeof(nova.id_placa) - 1);
  nova.id_placa[sizeof(nova.id_placa) - 1] = '\0';   // o \0 e obrigatorio
  nova.temp_c = temp_c;
  nova.umidade_pct = umidade_pct;
  strncpy(nova.dt_leitura, quando, sizeof(nova.dt_leitura) - 1);
  nova.dt_leitura[sizeof(nova.dt_leitura) - 1] = '\0';
  return nova.id;
}

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println();
  Serial.println("2o trimestre, dia 4, aula 2 - INSERT e SELECT");
  Serial.println("=================================================");
  Serial.println("o vetor abaixo e a tb_leitura: 5 linhas, como o banco.");
  Serial.println();
  Serial.println("id | placa     | temp_c | umid | dt_leitura");
  Serial.println("---+-----------+--------+------+-----------------------------");
}

void loop() {
  if (total_gravado < TOTAL_DE_LEITURAS) {
    int bruto = analogRead(PIN_SENSOR_ANALOGICO);
    float temp_c = (bruto / 4095.0f) * 50.0f;
    int umidade_pct = 55 + (total_gravado % 6);

    char quando[20];
    snprintf(quando, sizeof(quando), "2026-09-30 10:0%d:00", total_gravado);

    // INSERT
    int id = inserir("esp32-lab01", temp_c, umidade_pct, quando);
    if (id < 0) {
      Serial.println("INSERT recusado: a tabela esta cheia (5 linhas).");
      Serial.println("no dia 7 a tabela cresce ate 172800 linhas por dia.");
    }
  }

  // SELECT: imprime a tabela inteira, do id mais baixo para o mais alto.
  // ORDER BY id e essa ordem. No dia 7 a ordem muda, e muda de proposito.
  for (int i = 0; i < total_gravado; i++) {
    Serial.print("   ");
    Serial.print(tb_leitura[i].id);
    Serial.print(" | ");
    Serial.print(tb_leitura[i].id_placa);
    for (int e = strlen(tb_leitura[i].id_placa); e < 9; e++) Serial.print(" ");
    Serial.print(" | ");
    Serial.print(tb_leitura[i].temp_c, 1);
    for (int e = 1; e < 6; e++) Serial.print(" ");
    Serial.print(" | ");
    Serial.print(tb_leitura[i].umidade_pct);
    for (int e = 1; e < 4; e++) Serial.print(" ");
    Serial.print(" | ");
    Serial.println(tb_leitura[i].dt_leitura);
  }

  if (total_gravado >= TOTAL_DE_LEITURAS) {
    Serial.println("a tabela esta cheia. DELETE o dia 7, aqui seria so");
    Serial.println("reduzir TOTAL_DE_LEITURAS para zero e recarregar a placa.");
  } else {
    Serial.println("...");
  }
  delay(1500);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia04 aula2.