Consultas SQL
Seleção, junções, NULL, agregação e resultados sem duplicações acidentais.
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:
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);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:
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);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.
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);Executa a consulta para ver as tabelas.
As linhas que chegam ao agrupamento são:
| nome | e.id |
|---|---|
| Ana | 100 |
| Ana | 101 |
| Rui | 102 |
| Mia | NULL |
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.
| p | NOT p |
|---|---|
| verdadeiro | falso |
| falso | verdadeiro |
| desconhecido | desconhecido |
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.
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);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:
| Encomenda | Valores das linhas, em cêntimos | Total | Total > 10000 |
|---|---|---|---|
| 100 | 9000 + 2500 | 11500 | sim |
| 101 | 18000 | 18000 | sim |
| 102 | 7500 | 7500 | nã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.
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;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.
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
Mais uma pista
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
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.
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.
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);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.
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.
-- 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);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.
