Dia 5 — Consultar: SELECT
SELECT e a leitura das linhas
SELECT e a leitura das linhas
SELECT lê, INSERT grava. A forma mais simples é SELECT * FROM tarefas, e o resultado não é uma lista: é um objeto com rows, um array vazio quando nada casou com o filtro e as linhas quando casou.
const [resultado] = await banco.executeSql('SELECT * FROM tarefas'); const linhas = resultado.rows; const quantas = resultado.rows.length; if (linhas.length === 0) { // tela vazia: o app precisa ter um estado para esse caso }
O nome da propriedade é rows, e dentro dela está a linha. O .length é o número de linhas que casaram, e é ele que decide entre a tela com dado e a tela vazia. Ler é consultar o banco, e consultar é o verbo do trimestre: a mesma palavra que dá nome ao SELECT.
O array vazio é resultado normal, não erro. A consulta que não acha nada rodou com sucesso e por isso não rejeita a promessa — o aplicativo é quem decide o que a tela vazia mostra. Tratar o "não achei" como falha é o erro que faz a tela piscar e sumir.
A distinção entre "não achou" e "deu erro" é a que o try e o catch separam, e é ela que a tela precisa preservar: no primeiro caso o catch não roda, o rows chega vazio e a tela mostra o estado vazio; no segundo o catch roda e a tela mostra a mensagem de falha. Tratar os dois com o mesmo código é a causa mais comum de tela que pisca ao abrir.
Mapear resultado para a tela
O que volta do SELECT * traz o nome de toda coluna, e o que a tela precisa costuma ser um objeto menor. A etapa de mapear resultado é onde o banco vira dado de aplicativo:
const [resultado] = await banco.executeSql('SELECT * FROM tarefas ORDER BY prazo'); const tarefas = resultado.rows.map((linha) => ({ id: linha.id, titulo: linha.titulo, prazo: linha.prazo, feita: linha.feita === 1, }));
Duas decisões acontecem nessa linha. A primeira é linha.feita === 1: o banco devolve inteiro e o estado do aplicativo é booleano; sem o === 1, o valor 1 e a string '0' passam pelo filtro da tela com resultado errado. A segunda é nomear as colunas na consulta: SELECT id, titulo, prazo, feita FROM tarefas devolve exatamente o que a tela usa, e a consulta documenta a si mesma.
O exemplo desta página mostra o mapear resultado com a entrada e a saída lado a lado: as três linhas do banco entram no map e saem como três objetos com feita já booleano — pendente, feito, pendente. É essa a transformação que a tela consome, e ela acontece antes do setState, nunca dentro dele.
O mapeamento é também o ponto de trazer para a tela: enquanto o dado é linha de banco, ele pertence ao SQL; depois do map, ele é objeto de aplicativo, e a partir desse ponto nenhuma tela precisa saber que existe um banco no projeto.
Uma linha só
SELECT * FROM tarefas WHERE id = ? com o id da linha devolve uma linha, e resultado.rows[0] é ela — a primeira linha do resultado. Quando a linha não existe, rows é um array vazio e rows[0] é undefined: o acesso direto quebra o aplicativo com erro de propriedade de undefined, e o certo é conferir o tamanho antes de ler linha.
A checagem que resolve é a comparação com 0, e ela é o que separa "não existe" de "existe". O rows[0] de uma consulta que não achou nada é undefined, e ler .titulo de undefined é o erro que o aluno vê como TypeError na tela de edição:
const [resultado] = await banco.executeSql('SELECT * FROM tarefas WHERE id = ?', [idProcurado]); const linha = resultado.rows.length > 0 ? resultado.rows[0] : null;
Para várias linhas, a forma do aplicativo é sempre a mesma: consulta, mapeia para estado, setTarefas com o array novo. A tela nunca lê do banco sozinha; ela lê do estado, e quem encheu o estado foi a consulta.
Exemplo
// `SELECT` devolve um objeto com `rows`: um array de linhas. O array // vazio e resultado normal de uma consulta que nao achou nada. function executarSelect(sql, banco) { const achado = /SELECT (.+) FROM (\w+)(.*)$/i.exec(sql); if (!achado) throw new Error('SQL invalido: ' + sql); // o que sobra depois do nome da tabela e o filtro: e o `WHERE` que decide // quais linhas entram no resultado const [, listaDeColunas, tabela, resto] = achado; const nomes = listaDeColunas === '*' ? null : listaDeColunas.split(',').map((c) => c.trim()); let linhas = [...banco[tabela]]; const filtro = /WHERE (\w+) = (.+)$/i.exec(resto); if (filtro) { const [, coluna, valor] = filtro; linhas = linhas.filter((linha) => { // o valor do `WHERE` chega como texto e a coluna guarda o tipo que // o `INSERT` deu a ela: comparar `1` com `'1'` sem converter nunca // casa. E o filtro que precisa ser lido: ignorado, `WHERE id = 99` // devolve a tabela inteira e a tela inventa tarefa que nao existe. const daLinha = linha[coluna]; const doTexto = typeof daLinha === 'number' ? Number(valor) : typeof daLinha === 'boolean' ? valor === '1' : String(valor); return daLinha === doTexto; }); } if (nomes) linhas = linhas.map((linha) => { const nova = {}; for (const nome of nomes) nova[nome] = linha[nome]; return nova; }); return [{ rows: linhas }]; } const banco = { tarefas: [ { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 }, { id: 2, titulo: 'Ler o capitulo 4', prazo: '2026-09-12', feita: 1 }, { id: 3, titulo: 'Enviar o relatorio', prazo: '2026-09-08', feita: 0 }, ], }; const [resultado] = executarSelect('SELECT * FROM tarefas', banco); console.log('rows e do tipo', Array.isArray(resultado.rows) ? 'array' : typeof resultado.rows); console.log('numero de linhas:', resultado.rows.length); console.log('primeira linha:', resultado.rows[0]); // a consulta que nao acha nada devolve array vazio, e nao erro const [vazio] = executarSelect('SELECT * FROM tarefas WHERE id = 99', banco); console.log('consulta sem resultado:', vazio.rows, '| tamanho', vazio.rows.length); // mapear a linha do banco para o dado que a tela usa: `feita` chega como // inteiro e sai do mapeamento como booleano const tarefas = resultado.rows.map((linha) => ({ id: linha.id, titulo: linha.titulo, prazo: linha.prazo, feita: linha.feita === 1, })); console.log('mapeado:', tarefas.map((t) => t.feita ? 'feito' : 'pendente')); // uma linha so: `rows[0]` e ela, e `rows[0]` de uma consulta vazia e undefined console.log('rows[0] da consulta com id 99:', executarSelect('SELECT * FROM tarefas WHERE id = 99', banco)[0].rows[0]); // nomear as colunas devolve so o que a tela usa const [curtas] = executarSelect('SELECT id, titulo FROM tarefas', banco); console.log('colunas nomeadas:', curtas.rows[0]);
Saída real
rows e do tipo array
numero de linhas: 3
primeira linha: { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 }
consulta sem resultado: [] | tamanho 0
mapeado: [ 'pendente', 'feito', 'pendente' ]
rows[0] da consulta com id 99: undefined
colunas nomeadas: { id: 1, titulo: 'Revisar o WHERE' }
WHERE: filtrar o que vem
WHERE: filtrar o que vem
O WHERE é a cláusula que transforma uma consulta em outra. Fica depois do FROM e recebe uma condição que é avaliada linha por linha: a linha que satisfaz volta, a que não fica de fora.
SELECT * FROM tarefas WHERE feita = 0;
Os operadores de comparação são =, != (e o equivalente <>), >, <, >= e <=. Em conjunto: AND, OR e NOT, com parênteses quando a combinação tem precedência que muda o resultado — WHERE feita = 0 AND prazo < '2026-11-01' e WHERE feita = 0 OR prazo < '2026-11-01' devolvem conjuntos diferentes justamente porque o AND e mais forte.
A condição é o único texto que separa uma consulta da outra, e é por isso que ela fica no meio: o nome da tabela vem antes, a ordenação vem depois. Quando o WHERE cresce até caber numa tela, ele costuma ser o sinal de que falta uma tabela de ligação ou uma coluna que resolve a pergunta.
Dois operadores que resolvem perguntas que = não resolve: IN recebe a lista, WHERE id IN (1, 3, 7), e LIKE casa padrão de texto com % no lugar de qualquer sequência.
SELECT * FROM tarefas WHERE titulo LIKE 'Revisar%'; SELECT * FROM tarefas WHERE prazo < '2026-11-01' ORDER BY prazo ASC LIMIT 5;
Texto em LIKE e em comparação de data vai entre aspas simples; número, sem. prazo < '2026-11-01' funciona porque o SQLite compara texto ISO — AAAA-MM-DD ordena igual à ordem cronológica, desde que o formato não mude. O dia por extenso quebra isso.
O formato de data é a armadilha mais cara dessa aula, porque a coluna foi criada como TEXT e aceita qualquer coisa. '10/09/2026' é aceito sem erro, '< compara letra por letra, e a ordenação fica em ordem de dia em vez de ordem de mês. A comparação funciona quando o formato é constante, e quebra quando alguém digita no formato do país dele.
ORDER BY, ASC, DESC e LIMIT
ORDER BY ordena o resultado, ASC é o padrão e DESC inverte. LIMIT corta no fim. As três juntas formam a paginação de consulta: LIMIT 20 OFFSET 40 traz a terceira página de vinte linhas, e OFFSET sozinho com ordenação estável dá sempre a mesma página.
SELECT * FROM tarefas WHERE feita = 0 ORDER BY prazo ASC LIMIT 20 OFFSET 40;
A ordem das três cláusulas é fixa: WHERE filtra, ORDER BY organiza, LIMIT corta. Inverter produz erro de sintaxe, e o LIMIT antes do ORDER BY produz resultado sem sentido — o corte acontece antes da ordenação, e o que volta é as vinte primeiras linhas na ordem em que o banco as guardou.
Ordenar por coluna indexada é barato; sem índice, o motor compara todas as linhas e depois descarta. É por isso que o ORDER BY entra na lista de colunas que merecem índice, e não só o WHERE.
O exemplo desta página roda os filtros sobre cinco tarefas e imprime o conjunto que cada condição devolve: feita = 0, a comparação de prazo, a condição composta com AND, a lista com IN e o padrão de texto com LIKE. O ORDER BY prazo LIMIT 2 OFFSET 2 fecha a página mostrando o recorte no meio da lista, que é a posição em que a paginação real começa.
Comparar o filtro no código
A mesma condição em JavaScript é um filter com uma função que devolve booleano. A vantagem do WHERE não é a velocidade do laço: é que a condição é escrita uma vez, no texto, e serve para qualquer consulta — inclusive as que rodam sem passar pelo aplicativo. O filter no JavaScript tem o mesmo resultado, com o filtro do lado da tela.
A diferença que aparece no volume: com cinco linhas, as duas formas dão a mesma resposta instantaneamente. Com cinco mil, o WHERE entrega só o que casou e o filter entrega as cinco mil ao JavaScript para ele descartar quase todas. E há um caso em que só o WHERE resolve: quando a consulta não é do aplicativo — um relatório rodado direto no banco, ou outra tela que precisa do mesmo filtro sem passar pelo seu código.
Exemplo
// `WHERE` filtra linha por linha: a linha que satisfaz a condicao volta, // a que nao satisfaz fica de fora. O mesmo filtro, escrito no texto da // consulta e escrito em JavaScript, devolve o mesmo conjunto. function valorDe(texto, linha) { const alvo = String(texto).trim(); if (/^\d+$/.test(alvo)) return Number(alvo); if (alvo.startsWith("'")) return alvo.slice(1, -1); if (linha && alvo in linha) return linha[alvo]; return alvo; } function compara(esquerda, operador, direita) { if (operador === '=') return esquerda === direita; if (operador === '!=' || operador === '<>') return esquerda !== direita; if (operador === '>') return esquerda > direita; if (operador === '<') return esquerda < direita; if (operador === '>=') return esquerda >= direita; if (operador === '<=') return esquerda <= direita; if (operador === 'LIKE') { return new RegExp('^' + String(direita).replace(/%/g, '.*') + '$').test(String(esquerda)); } if (operador === 'IN') { return String(direita).replace(/[()']/g, '').split(',').map((v) => v.trim()).includes(esquerda); } throw new Error('operador desconhecido: ' + operador); } function satisfaz(condicao, linha) { return condicao.split(/\s+AND\s+/).every((parte) => { const achado = parte.trim().match(/^(\w+)\s*(>=|<=|!=|<>|=|>|<|LIKE|IN)\s*(.+)$/i); if (!achado) throw new Error('condicao nao reconhecida: ' + parte); const [, coluna, operador, bruto] = achado; return compara(valorDe(bruto, linha), operador.toUpperCase(), valorDe(bruto, linha)); }); } const banco = { tarefas: [ { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 }, { id: 2, titulo: 'Ler o capitulo 4', prazo: '2026-09-12', feita: 1 }, { id: 3, titulo: 'Enviar o relatorio', prazo: '2026-09-08', feita: 0 }, { id: 4, titulo: 'Comprar cafe', prazo: '2026-09-07', feita: 0 }, { id: 5, titulo: 'Revisar o GROUP BY', prazo: '2026-09-11', feita: 0 }, ], }; function consultar(consulta) { let linhas = [...banco[consulta.from]]; if (consulta.where) linhas = linhas.filter((linha) => satisfaz(consulta.where, linha)); if (consulta.orderBy) { linhas.sort((a, b) => { const x = a[consulta.orderBy], y = b[consulta.orderBy]; return x === y ? 0 : (x > y ? 1 : -1) * (consulta.desc ? -1 : 1); }); } if (consulta.limit) linhas = linhas.slice(consulta.offset || 0, (consulta.offset || 0) + consulta.limit); return linhas; } const pendentes = consultar({ from: 'tarefas', where: 'feita = 0' }); console.log('WHERE feita = 0 devolveu', pendentes.length, 'de', banco.tarefas.length); const vencidas = consultar({ from: 'tarefas', where: "prazo <= '2026-09-09'" }); console.log("prazo <= '2026-09-09':", vencidas.map((l) => l.titulo)); const composta = consultar({ from: 'tarefas', where: "feita = 0 AND prazo <= '2026-09-09'" }); console.log('feita = 0 AND prazo <= 09-09:', composta.map((l) => l.titulo)); const conjunto = consultar({ from: 'tarefas', where: 'id IN (1, 3, 5)' }); console.log('id IN (1, 3, 5):', conjunto.map((l) => l.id)); const comLike = consultar({ from: 'tarefas', where: "titulo LIKE 'Revisar%'" }); console.log("titulo LIKE 'Revisar%':", comLike.map((l) => l.titulo)); const primeiraPagina = consultar({ from: 'tarefas', where: 'feita = 0', orderBy: 'prazo', limit: 2, offset: 2 }); console.log('ORDER BY prazo LIMIT 2 OFFSET 2:', primeiraPagina.map((l) => l.prazo));
Saída real
WHERE feita = 0 devolveu 5 de 5 prazo <= '2026-09-09': [ 'Revisar o WHERE', 'Ler o capitulo 4', 'Enviar o relatorio', 'Comprar cafe', 'Revisar o GROUP BY' ] feita = 0 AND prazo <= 09-09: [ 'Revisar o WHERE', 'Ler o capitulo 4', 'Enviar o relatorio', 'Comprar cafe', 'Revisar o GROUP BY' ] id IN (1, 3, 5): [] titulo LIKE 'Revisar%': [ 'Revisar o WHERE', 'Ler o capitulo 4', 'Enviar o relatorio', 'Comprar cafe', 'Revisar o GROUP BY' ] ORDER BY prazo LIMIT 2 OFFSET 2: [ '2026-09-10', '2026-09-11' ]