Consultas SQL

Seleção, junções, NULL, agregação e resultados sem duplicações acidentais.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

A encomenda 100 tem duas linhas de compra. Contar essas linhas dá 2, mas contar encomendas dá 1. Antes de escrever SQL, define o que representa uma linha da resposta. A consulta declara esse resultado; o SGBD escolhe como o obter.

SELECT e filtros

SELECT id, nome, precoCentimos / 100.0 AS euros
FROM Produto
WHERE precoCentimos > 3000
ORDER BY precoCentimos DESC, id;

A resposta contém Monitor/180 euros e Teclado/45 euros, nesta ordem. WHERE testa o preço em cêntimos; a lista de SELECT divide-o para mostrar euros. Uma coluna usada no filtro não tem de aparecer no resultado.

AS atribui um nome ao resultado de uma expressão. Um alias de tabela, como Produto AS p, abrevia referências e permite usar a mesma tabela com papéis diferentes. Não altera os dados nem o nome permanente da tabela.

BETWEEN a AND b inclui ambos os limites. LIKE 'Ra%' procura uma sequência que começa por Ra. O símbolo % representa zero ou mais caracteres e _ representa um carácter.

Em SQLite, LIKE é normalmente insensível à diferença de maiúsculas ASCII, enquanto = segue a collation escolhida. Não assumes o mesmo comportamento para todos os caracteres Unicode ou todos os motores.

Combina predicados com AND, OR e NOT. Usa parênteses quando misturas condições: categoria = 'A' AND (stock = 0 OR precoCentimos > 10000) exige categoria A em ambos os casos. Sem os parênteses, o segundo caso poderia aceitar outra categoria.

Duplicados e ordem

SQL usa multiconjuntos por omissão. Se projetarmos idCliente de Encomenda, Ana aparece duas vezes. SELECT DISTINCT idCliente elimina a repetição. DISTINCT aplica-se à combinação completa das colunas escolhidas.

Eliminar nomes repetidos pode juntar pessoas diferentes. Para obter uma linha por cliente, escolhe o seu id e o nome ou usa uma condição de existência sobre Cliente. Não uses DISTINCT para esconder uma junção errada.

A ordem de uma tabela ou consulta não é garantida sem ORDER BY. LIMIT 1 sem uma ordem definida escolhe uma linha sem significado estável.

Se houver empates, acrescenta um critério de desempate quando a pergunta exigir exatamente uma linha. Se pedir todos os máximos, não os elimines com LIMIT 1.

Junções com condição

Uma junção liga linhas que satisfazem ON. Qualifica nomes ambíguos e escreve a ligação de cada par de tabelas:

Ficheiros do exemplo
Cada linha comprada com o seu cliente e produto
SELECT e.id AS encomenda, c.nome AS cliente,
     p.nome AS produto, i.qtd,
     i.qtd * i.precoUnitario AS totalCentimos
FROM Encomenda AS e
JOIN Cliente AS c ON c.id = e.idCliente
JOIN Item AS i ON i.idEncomenda = e.id
JOIN Produto AS p ON p.id = i.idProduto
ORDER BY e.id, p.id;
PRAGMA foreign_keys = ON;
CREATE TABLE Cliente (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  email TEXT UNIQUE
);
CREATE TABLE Produto (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  precoCentimos INTEGER NOT NULL CHECK (precoCentimos >= 0),
  stock INTEGER NOT NULL CHECK (stock >= 0)
);
CREATE TABLE Encomenda (
  id INTEGER PRIMARY KEY,
  data TEXT NOT NULL,
  idCliente INTEGER NOT NULL REFERENCES Cliente(id)
);
CREATE TABLE Item (
  idEncomenda INTEGER NOT NULL REFERENCES Encomenda(id) ON DELETE CASCADE,
  idProduto INTEGER NOT NULL REFERENCES Produto(id),
  qtd INTEGER NOT NULL CHECK (qtd > 0),
  precoUnitario INTEGER NOT NULL CHECK (precoUnitario >= 0),
  PRIMARY KEY (idEncomenda, idProduto)
);
INSERT INTO Cliente VALUES
  (1, 'Ana', 'ana@example.test'),
  (2, 'Rui', NULL),
  (3, 'Mia', NULL);
INSERT INTO Produto VALUES
  (10, 'Teclado', 4500, 20),
  (11, 'Rato', 2500, 50),
  (12, 'Monitor', 18000, 5);
INSERT INTO Encomenda VALUES
  (100, '2026-01-05', 1),
  (101, '2026-01-06', 1),
  (102, '2026-01-06', 2);
INSERT INTO Item VALUES
  (100, 10, 2, 4500),
  (100, 11, 1, 2500),
  (101, 12, 1, 18000),
  (102, 11, 3, 2500);
Resultado

Executa a consulta para ver as tabelas.

A resposta tem quatro linhas: duas da encomenda 100, uma da 101 e uma da 102. Cada linha corresponde a um Item. Ana aparece três vezes porque comprou três pares encomenda/produto, não porque sejam três clientes.

Omitir uma condição pode criar um produto cartesiano. Juntar três clientes com três encomendas sem condição produz nove pares, incluindo associações falsas.

NATURAL JOIN não é uma correção automática. Cliente e Encomenda têm ambos id, com significados distintos.

JOIN ... USING(idCliente) exige uma coluna desse nome em ambos os lados e devolve uma só cópia dessa coluna. ON permite nomes diferentes e condições mais gerais.

Pares sem repetição na mesma tabela

Para formar pares de encomendas do mesmo cliente, juntamos Encomenda consigo própria. Os aliases dão papéis diferentes à mesma tabela:

Ficheiros do exemplo
Pares de encomendas do mesmo cliente
SELECT e1.id AS primeira, e2.id AS segunda
FROM Encomenda AS e1
JOIN Encomenda AS e2
ON e2.idCliente = e1.idCliente AND e1.id < e2.id
ORDER BY primeira, segunda;
PRAGMA foreign_keys = ON;
CREATE TABLE Cliente (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  email TEXT UNIQUE
);
CREATE TABLE Produto (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  precoCentimos INTEGER NOT NULL CHECK (precoCentimos >= 0),
  stock INTEGER NOT NULL CHECK (stock >= 0)
);
CREATE TABLE Encomenda (
  id INTEGER PRIMARY KEY,
  data TEXT NOT NULL,
  idCliente INTEGER NOT NULL REFERENCES Cliente(id)
);
CREATE TABLE Item (
  idEncomenda INTEGER NOT NULL REFERENCES Encomenda(id) ON DELETE CASCADE,
  idProduto INTEGER NOT NULL REFERENCES Produto(id),
  qtd INTEGER NOT NULL CHECK (qtd > 0),
  precoUnitario INTEGER NOT NULL CHECK (precoUnitario >= 0),
  PRIMARY KEY (idEncomenda, idProduto)
);
INSERT INTO Cliente VALUES
  (1, 'Ana', 'ana@example.test'),
  (2, 'Rui', NULL),
  (3, 'Mia', NULL);
INSERT INTO Produto VALUES
  (10, 'Teclado', 4500, 20),
  (11, 'Rato', 2500, 50),
  (12, 'Monitor', 18000, 5);
INSERT INTO Encomenda VALUES
  (100, '2026-01-05', 1),
  (101, '2026-01-06', 1),
  (102, '2026-01-06', 2);
INSERT INTO Item VALUES
  (100, 10, 2, 4500),
  (100, 11, 1, 2500),
  (101, 12, 1, 18000),
  (102, 11, 3, 2500);
Resultado

Executa a consulta para ver as tabelas.

A resposta é só o par 100 e 101, as duas encomendas de Ana. A condição e1.id < e2.id exclui pares de uma encomenda consigo própria e conserva cada par uma vez. Com e1.id <> e2.id, obteríamos também o par 101 e 100, a mesma combinação por outra ordem. Sem a condição de cliente, o par misturaria encomendas de pessoas diferentes.

Junções externas e clientes sem compras

Um INNER JOIN elimina linhas sem correspondência. Um LEFT JOIN conserva todas as linhas da esquerda e preenche as colunas da direita com NULL quando não há correspondência.

Ficheiros do exemplo
Conservar Mia na contagem de encomendas
SELECT c.id, c.nome, COUNT(e.id) AS encomendas
FROM Cliente AS c
LEFT JOIN Encomenda AS e ON e.idCliente = c.id
GROUP BY c.id, c.nome
ORDER BY c.id;
PRAGMA foreign_keys = ON;
CREATE TABLE Cliente (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  email TEXT UNIQUE
);
CREATE TABLE Produto (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  precoCentimos INTEGER NOT NULL CHECK (precoCentimos >= 0),
  stock INTEGER NOT NULL CHECK (stock >= 0)
);
CREATE TABLE Encomenda (
  id INTEGER PRIMARY KEY,
  data TEXT NOT NULL,
  idCliente INTEGER NOT NULL REFERENCES Cliente(id)
);
CREATE TABLE Item (
  idEncomenda INTEGER NOT NULL REFERENCES Encomenda(id) ON DELETE CASCADE,
  idProduto INTEGER NOT NULL REFERENCES Produto(id),
  qtd INTEGER NOT NULL CHECK (qtd > 0),
  precoUnitario INTEGER NOT NULL CHECK (precoUnitario >= 0),
  PRIMARY KEY (idEncomenda, idProduto)
);
INSERT INTO Cliente VALUES
  (1, 'Ana', 'ana@example.test'),
  (2, 'Rui', NULL),
  (3, 'Mia', NULL);
INSERT INTO Produto VALUES
  (10, 'Teclado', 4500, 20),
  (11, 'Rato', 2500, 50),
  (12, 'Monitor', 18000, 5);
INSERT INTO Encomenda VALUES
  (100, '2026-01-05', 1),
  (101, '2026-01-06', 1),
  (102, '2026-01-06', 2);
INSERT INTO Item VALUES
  (100, 10, 2, 4500),
  (100, 11, 1, 2500),
  (101, 12, 1, 18000),
  (102, 11, 3, 2500);
Resultado

Executa a consulta para ver as tabelas.

As linhas que chegam ao agrupamento são:

nomee.id
Ana100
Ana101
Rui102
MiaNULL

A resposta é Ana com 2, Rui com 1 e Mia com 0, porque COUNT(e.id) ignora o NULL da linha artificial de Mia, enquanto COUNT(*) contaria essa linha.

Se quisermos conservar clientes sem encomendas numa data, coloca o filtro da data no ON:

FROM Cliente AS c
LEFT JOIN Encomenda AS e
  ON e.idCliente = c.id AND e.data = '2026-01-05'

Mover e.data = ... para WHERE retira as linhas sem correspondência, pois a comparação com NULL não é verdadeira. Uma condição no ON decide correspondências; uma condição no WHERE filtra o resultado já produzido.

RIGHT JOIN conserva o lado direito e FULL OUTER JOIN conserva ambos os lados. SQLite inclui estes operadores a partir de 3.39; em versões anteriores, usa reescritas adequadas. Todos os exemplos executáveis destas páginas usam LEFT JOIN quando precisam de preservar linhas sem correspondência.

NULL e lógica a três valores

Uma comparação com NULL, incluindo NULL = NULL, normalmente dá desconhecido. WHERE conserva apenas verdadeiro, descartando falso e desconhecido. Para testar ausência, usa IS NULL ou IS NOT NULL.

pNOT p
verdadeirofalso
falsoverdadeiro
desconhecidodesconhecido

falso AND desconhecido é falso; verdadeiro AND desconhecido é desconhecido. verdadeiro OR desconhecido é verdadeiro; falso OR desconhecido é desconhecido. Assim, preco > 30 OR preco <= 30 não inclui automaticamente uma linha cujo preço seja NULL.

COALESCE(x, alternativa) devolve o primeiro argumento não nulo. Só transforma um desconhecido em zero quando zero for a interpretação certa. Um stock desconhecido não significa que não existe stock.

GROUP BY e HAVING

GROUP BY reúne linhas com os mesmos valores das colunas de agrupamento. As funções de agregação calculam um resultado para cada grupo. WHERE filtra linhas antes do agrupamento; HAVING filtra grupos depois.

Ficheiros do exemplo
Encomendas que ultrapassam 100 euros
SELECT idEncomenda, SUM(qtd * precoUnitario) / 100.0 AS euros
FROM Item
GROUP BY idEncomenda
HAVING SUM(qtd * precoUnitario) > 10000
ORDER BY idEncomenda;
PRAGMA foreign_keys = ON;
CREATE TABLE Cliente (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  email TEXT UNIQUE
);
CREATE TABLE Produto (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  precoCentimos INTEGER NOT NULL CHECK (precoCentimos >= 0),
  stock INTEGER NOT NULL CHECK (stock >= 0)
);
CREATE TABLE Encomenda (
  id INTEGER PRIMARY KEY,
  data TEXT NOT NULL,
  idCliente INTEGER NOT NULL REFERENCES Cliente(id)
);
CREATE TABLE Item (
  idEncomenda INTEGER NOT NULL REFERENCES Encomenda(id) ON DELETE CASCADE,
  idProduto INTEGER NOT NULL REFERENCES Produto(id),
  qtd INTEGER NOT NULL CHECK (qtd > 0),
  precoUnitario INTEGER NOT NULL CHECK (precoUnitario >= 0),
  PRIMARY KEY (idEncomenda, idProduto)
);
INSERT INTO Cliente VALUES
  (1, 'Ana', 'ana@example.test'),
  (2, 'Rui', NULL),
  (3, 'Mia', NULL);
INSERT INTO Produto VALUES
  (10, 'Teclado', 4500, 20),
  (11, 'Rato', 2500, 50),
  (12, 'Monitor', 18000, 5);
INSERT INTO Encomenda VALUES
  (100, '2026-01-05', 1),
  (101, '2026-01-06', 1),
  (102, '2026-01-06', 2);
INSERT INTO Item VALUES
  (100, 10, 2, 4500),
  (100, 11, 1, 2500),
  (101, 12, 1, 18000),
  (102, 11, 3, 2500);
Resultado

Executa a consulta para ver as tabelas.

Os quatro Items formam três grupos. A soma calcula o valor de cada encomenda; só depois HAVING testa o total:

EncomendaValores das linhas, em cêntimosTotalTotal > 10000
1009000 + 250011500sim
1011800018000sim
10275007500não

A resposta contém 100/115 euros e 101/180 euros. Um filtro sobre o preço de cada linha antes da soma faria outra pergunta. WHERE SUM(...) > ... não filtra o total de um grupo.

COUNT(*) conta linhas. COUNT(x) conta os x não nulos; COUNT(DISTINCT x) conta valores distintos não nulos. SUM, AVG, MIN e MAX ignoram valores nulos. Sem linhas e sem GROUP BY, COUNT dá 0, enquanto SUM, AVG, MIN e MAX dão NULL. Com GROUP BY, uma entrada vazia não produz grupos.

Agregações com NULL e com entrada vazia
CREATE TABLE Valor(x INTEGER);
INSERT INTO Valor VALUES (10), (NULL), (20);
SELECT COUNT(*) AS linhas, COUNT(x) AS conhecidos,
     SUM(x) AS soma, AVG(x) AS media FROM Valor;
SELECT COUNT(*) AS linhas,
     SUM(x) AS soma
FROM Valor WHERE 0;
SELECT x, COUNT(*) FROM Valor WHERE 0 GROUP BY x;
Resultado

Executa a consulta para ver as tabelas.

A primeira resposta tem 3 linhas, 2 valores conhecidos, soma 30 e média 15 porque a média divide por dois valores conhecidos, não por três linhas. A segunda tem contagem 0 e soma NULL e a terceira não devolve linhas.

SQLite permite colunas soltas numa consulta agregada, mas o valor escolhido pode não representar o grupo de forma estável. Há regras especiais para certos usos de MIN e MAX, que ainda deixam ambiguidades com empates. Escreve cada coluna da resposta no agrupamento ou numa agregação, ou formula explicitamente a escolha com uma subconsulta. Não uses essa permissividade para justificar SQL portátil.

Multiplicação de linhas e contagens

Se juntarmos Cliente, Encomenda e Item, a encomenda 100 aparece duas vezes porque tem dois produtos. COUNT(e.id) passa a contar linhas compradas, dando 3 para Ana. Para contar encomendas nesse resultado, usa COUNT(DISTINCT e.id); para somar valores de encomendas já agregados, evita uma junção que volte a multiplicá-los.

Agrupar por nome também funde pessoas de nome igual. Agrupa por chave e inclui o nome. Cada junção deve ter uma cardinalidade esperada antes de calculares a agregação.

Expressões e datas

CASE calcula valores por condições:

SELECT nome,
       CASE WHEN stock = 0 THEN 'esgotado'
            WHEN stock < 10 THEN 'baixo'
            ELSE 'disponível' END AS estado
FROM Produto;

No exemplo, Monitor tem stock baixo; Teclado e Rato estão disponíveis. Um CASE sem ELSE devolve NULL quando nenhuma condição corresponde.

Datas ISO guardadas como texto, no formato AAAA-MM-DD, podem ser comparadas lexicograficamente se o formato for uniforme e válido. Para durações, usa funções de data, como julianday(fim) - julianday(inicio). Subtrair dois textos com datas não calcula dias. Inclui a decisão de contar ou não os dias inicial e final no enunciado.

Ordem lógica da consulta

Para explicar uma consulta, pensa em FROM e junções, depois WHERE, agrupamento, HAVING, expressões de SELECT, DISTINCT, ORDER BY e LIMIT. Esta é uma ordem de interpretação, não uma promessa sobre o plano físico do motor.

Quando uma resposta surpreender, observa a tabela intermédia antes de agregar. Quase sempre é mais útil conferir quais as linhas produzidas pela junção do que acrescentar outro DISTINCT no fim.

Como complemento, o vídeo de Rodrigo Dias reúne joins, subconsultas e CTEs.

Abrir no YouTube

Exercícios

Diagnosticar duas contagens de encomendas

Cliente contém Ana (1), Rui (2) e Mia (3). Encomenda contém 100 e 101 de Ana e 102 de Rui. Item tem duas linhas na 100, uma na 101 e uma na 102.

Prevê estas duas consultas e corrige-as para contar encomendas de todos os clientes, incluindo zero para Mia:

SELECT c.id, COUNT(*) AS n
FROM Cliente AS c LEFT JOIN Encomenda AS e ON e.idCliente = c.id
GROUP BY c.id;

SELECT c.id, COUNT(e.id) AS n
FROM Cliente AS c JOIN Encomenda AS e ON e.idCliente = c.id
JOIN Item AS i ON i.idEncomenda = e.id
GROUP BY c.id;

Pista
Enumera as linhas que chegam a cada agrupamento.
Mais uma pista
Mia gera uma linha artificial na primeira consulta; a encomenda 100 gera duas linhas na segunda.
Solução

A primeira devolve (1,2),(2,1),(3,1). COUNT(*) conta a linha artificial de Mia. Trocar por COUNT(e.id) dá zero para Mia, porque e.id é nulo apenas quando não existe correspondência.

A segunda devolve (1,3),(2,1): conta a 100 duas vezes e não cria grupo para Mia. Se Item não é necessário, retira essa junção e usa a primeira consulta corrigida. Se precisas de o conservar, usa duas junções externas e conta ids distintos:

SELECT c.id, COUNT(DISTINCT e.id) AS n
FROM Cliente AS c
LEFT JOIN Encomenda AS e ON e.idCliente = c.id
LEFT JOIN Item AS i ON i.idEncomenda = e.id
GROUP BY c.id ORDER BY c.id;

A resposta corrigida é (1,2),(2,1),(3,0).

Confere a tua resposta:

  • Previ Ana 2, Rui 1 e Mia 1 no primeiro SQL.
  • Expliquei o zero de Mia com COUNT(e.id).
  • Previ Ana 3 e Rui 1 após a junção a Item.
  • Corrigi a multiplicação e preservei clientes sem compras.

Erros frequentes

DISTINCT no SELECT exterior não corrige uma contagem já calculada. COUNT de COALESCE(e.id,0) voltaria a contar a linha de Mia.
Voltar à explicação
Agregação sem linhas

Com uma tabela T(x) vazia, prevê SELECT COUNT(*), SUM(x) FROM T e SELECT x, COUNT(*) FROM T GROUP BY x.

Pista

A primeira consulta é uma agregação global; a segunda precisa de grupos existentes.

Solução

A primeira devolve uma linha (0,NULL). A segunda não devolve linhas, porque não há grupos. COALESCE(SUM(x),0) pode apresentar zero quando esse significado for pretendido, mas não muda o facto de a agregação original ser NULL.

Confere a tua resposta:

  • Previ uma linha na agregação global e nenhuma na agrupada.
  • Distingui zero de NULL.
Voltar à explicação
Pares sem repetição na mesma tabela

Escreve SQL que devolva pares de encomendas distintas do mesmo cliente, cada par uma vez. A execução apresenta o resultado; confere que Rui e Mia não geram pares.

Completa a condição de junção. A resposta deve conter só o par 100 e 101.

Ficheiros do exemplo
Completar a autojunção de encomendas
SELECT e1.id AS primeira, e2.id AS segunda
FROM Encomenda AS e1
JOIN Encomenda AS e2
ON 0 -- Substitui pela condição que define cada par.
ORDER BY primeira, segunda;
PRAGMA foreign_keys = ON;
CREATE TABLE Cliente (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  email TEXT UNIQUE
);
CREATE TABLE Produto (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  precoCentimos INTEGER NOT NULL CHECK (precoCentimos >= 0),
  stock INTEGER NOT NULL CHECK (stock >= 0)
);
CREATE TABLE Encomenda (
  id INTEGER PRIMARY KEY,
  data TEXT NOT NULL,
  idCliente INTEGER NOT NULL REFERENCES Cliente(id)
);
CREATE TABLE Item (
  idEncomenda INTEGER NOT NULL REFERENCES Encomenda(id) ON DELETE CASCADE,
  idProduto INTEGER NOT NULL REFERENCES Produto(id),
  qtd INTEGER NOT NULL CHECK (qtd > 0),
  precoUnitario INTEGER NOT NULL CHECK (precoUnitario >= 0),
  PRIMARY KEY (idEncomenda, idProduto)
);
INSERT INTO Cliente VALUES
  (1, 'Ana', 'ana@example.test'),
  (2, 'Rui', NULL),
  (3, 'Mia', NULL);
INSERT INTO Produto VALUES
  (10, 'Teclado', 4500, 20),
  (11, 'Rato', 2500, 50),
  (12, 'Monitor', 18000, 5);
INSERT INTO Encomenda VALUES
  (100, '2026-01-05', 1),
  (101, '2026-01-06', 1),
  (102, '2026-01-06', 2);
INSERT INTO Item VALUES
  (100, 10, 2, 4500),
  (100, 11, 1, 2500),
  (101, 12, 1, 18000),
  (102, 11, 3, 2500);
Resultado

Executa a consulta para ver as tabelas.

Pista

Uma tabela com dois papéis precisa de dois nomes na mesma consulta.

Mais uma pista

Exclui pares de uma encomenda consigo própria e pares repetidos por ordem.

Solução

A condição é e2.idCliente = e1.idCliente AND e1.id < e2.id. Só Ana tem duas encomendas, por isso o resultado é o par 100 e 101. Com e1.id <> e2.id, o par apareceria nas duas ordens. Sem a condição de cliente, o resultado misturaria encomendas de pessoas diferentes.

Confere a tua resposta:

  • Usei dois aliases para dar papéis diferentes a Encomenda.
  • Exigi o mesmo cliente e e1.id < e2.id.
  • Previ só o par 100 e 101.

Erros frequentes

Não uses o mesmo alias nos dois lados da condição. Uma autojunção sem condição de ordem conta cada combinação duas vezes.

Voltar à explicação
Totais por cliente, incluindo quem não comprou

Escreve SQL que devolva, por cliente, o total comprado em euros, incluindo Mia com zero. A execução apresenta o resultado; confere os três valores.

Parte de Cliente e conserva clientes sem encomendas. Os totais são 29500, 7500 e 0 cêntimos.

Ficheiros do exemplo
Completar os totais por cliente
-- Completa as junções e a expressão do total.
SELECT c.id, c.nome FROM Cliente AS c ORDER BY c.id;
PRAGMA foreign_keys = ON;
CREATE TABLE Cliente (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  email TEXT UNIQUE
);
CREATE TABLE Produto (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  precoCentimos INTEGER NOT NULL CHECK (precoCentimos >= 0),
  stock INTEGER NOT NULL CHECK (stock >= 0)
);
CREATE TABLE Encomenda (
  id INTEGER PRIMARY KEY,
  data TEXT NOT NULL,
  idCliente INTEGER NOT NULL REFERENCES Cliente(id)
);
CREATE TABLE Item (
  idEncomenda INTEGER NOT NULL REFERENCES Encomenda(id) ON DELETE CASCADE,
  idProduto INTEGER NOT NULL REFERENCES Produto(id),
  qtd INTEGER NOT NULL CHECK (qtd > 0),
  precoUnitario INTEGER NOT NULL CHECK (precoUnitario >= 0),
  PRIMARY KEY (idEncomenda, idProduto)
);
INSERT INTO Cliente VALUES
  (1, 'Ana', 'ana@example.test'),
  (2, 'Rui', NULL),
  (3, 'Mia', NULL);
INSERT INTO Produto VALUES
  (10, 'Teclado', 4500, 20),
  (11, 'Rato', 2500, 50),
  (12, 'Monitor', 18000, 5);
INSERT INTO Encomenda VALUES
  (100, '2026-01-05', 1),
  (101, '2026-01-06', 1),
  (102, '2026-01-06', 2);
INSERT INTO Item VALUES
  (100, 10, 2, 4500),
  (100, 11, 1, 2500),
  (101, 12, 1, 18000),
  (102, 11, 3, 2500);
Resultado

Executa a consulta para ver as tabelas.

Pista

Junta encomendas e linhas sem perder clientes, depois agrupa por cliente.

Mais uma pista

A soma sobre linhas vazias dá NULL; converte para zero na apresentação.

Solução
SELECT c.id, c.nome,
       COALESCE(SUM(i.qtd * i.precoUnitario), 0) / 100.0 AS euros
FROM Cliente AS c
LEFT JOIN Encomenda AS e ON e.idCliente = c.id
LEFT JOIN Item AS i ON i.idEncomenda = e.id
GROUP BY c.id, c.nome ORDER BY c.id;

Ana soma 11500 + 18000 = 29500 cêntimos, Rui soma 7500 e Mia soma NULL, apresentado como 0. O agrupamento por id evita fundir pessoas com o mesmo nome. Se o filtro de datas fosse para WHERE, Mia desapareceria; no ON, conserva-se a linha sem correspondência.

Confere a tua resposta:

  • Conservei Mia com LEFT JOIN e agrupei por id.
  • Somei sobre linhas de Item, não sobre encomendas.
  • Obtive Ana 295, Rui 75 e Mia 0 euros.

Erros frequentes

Não contes encomendas quando a pergunta pede euros. Não filtres a tabela da direita no WHERE se quiseres conservar clientes sem compras.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.