SQL WITH (CTE)
Na aula de Subconsultas você colocou um SELECT dentro de outro. Funciona, mas quando a consulta de dentro cresce, o comando vira uma boneca russa difícil de ler: você precisa abrir os parênteses de dentro para fora para entender o que acontece. O WITH resolve isso: você calcula a parte auxiliar primeiro, dá um nome a ela e depois usa esse nome como se fosse uma tabela. Esse resultado com nome se chama CTE. Nesta aula a Ana vai montar, passo a passo, relatórios de vendas da Loja Aurora com ele.
- O que é uma CTE e como escrever o
WITH nome AS (…). - Como usar a CTE no
SELECTprincipal, emWHERE, emJOINe até duas vezes no mesmo comando. - Como encadear várias CTEs, cada uma usando a anterior.
- Como dar nome às colunas da CTE:
WITH t(a, b) AS (…). WITHantes de umDELETE.- CTE × subconsulta × view × tabela temporária: qual usar.
- Os erros mais comuns, com a mensagem real do SQLite.
Todos os exemplos rodam de verdade no seu navegador, no banco da Loja Aurora. O quadro "Resultado" embaixo de cada código mostra o que o SQLite devolveu.
A ideia: o rascunho na margem do caderno
Pense numa conta de matemática grande. Você não faz tudo numa linha só: primeiro faz uma conta pequena na margem do caderno, escreve "subtotal = 230" e depois usa o subtotal na conta principal. Fica mais fácil de fazer e de conferir.
A CTE é esse rascunho na margem. A sigla vem do inglês Common Table Expression, "expressão de tabela comum". Na prática, é uma consulta com nome, que vale só durante um comando:
| No caderno | No SQL |
|---|---|
| "Rascunho: subtotal = …" | WITH subtotal AS ( SELECT … ) |
| A conta principal, que usa o subtotal | o SELECT principal, que lê FROM subtotal |
| Virou a página, o rascunho não vale mais | terminou o comando (no ;), a CTE some |
WITH quer dizer "com": "com este rascunho chamado subtotal, faça a consulta tal". Ninguém cria tabela nenhuma no banco; a CTE existe só enquanto o comando roda.
1. A primeira CTE
Comece por algo pequeno: separar os produtos caros (acima de R$ 100) num rascunho chamado caros e depois ler esse rascunho.
WITH caros AS ( SELECT nome, preco FROM produtos WHERE preco > 100 ) SELECT nome, preco FROM caros ORDER BY preco DESC;
- WITH
- "Com". Avisa que, antes do comando principal, vêm uma ou mais CTEs.
- caros
- O nome que você escolheu para o rascunho. Siga as mesmas regras de um nome de tabela: minúsculas, sem espaço, dizendo o que tem dentro.
- AS (
- "Como". Entre os parênteses vai a consulta que monta o rascunho. Os parênteses são obrigatórios.
- SELECT nome, preco FROM produtos WHERE preco > 100
- Um
SELECTcomum, como os que você já escreve. As colunas que ele devolve (nomeepreco) viram as colunas da CTE. - )
- Fecha a CTE. Repare: não tem ponto e vírgula aqui. O comando ainda não acabou.
- SELECT nome, preco FROM caros
- O comando principal. Ele lê
caroscomo se fosse uma tabela de verdade. - ORDER BY preco DESC;
- A ordenação do resultado final, e agora sim o
;que encerra o comando inteiro.
O resultado tem os 4 produtos acima de R$ 100. Para um filtro tão simples, a CTE nem era necessária (um WHERE bastava). Ela mostra o seu valor quando o rascunho é uma conta de verdade, como na próxima seção.
2. Uma CTE que faz uma conta
A Ana quer saber quais pedidos passaram de R$ 500 em produtos. O valor de um pedido não está pronto em coluna nenhuma: ele é a soma de quantidade * preco_unitario dos itens (aula SUM()). Então o rascunho é exatamente essa conta, um total por pedido:
WITH totais AS (
SELECT pedido_id,
SUM(quantidade * preco_unitario) AS valor
FROM itens_pedido
GROUP BY pedido_id
)
SELECT pedido_id, valor
FROM totais
WHERE valor > 500
ORDER BY valor DESC;
- totais
- O rascunho: uma linha por pedido (90 linhas), com duas colunas.
- SUM(quantidade * preco_unitario) AS valor
- A conta de cada pedido. O apelido
valoré importante: é por ele que o comando principal vai chamar a coluna. - GROUP BY pedido_id
- Junta os itens de cada pedido numa linha só (aula GROUP BY).
- WHERE valor > 500
- O filtro no comando principal. Ele filtra o resultado da soma, coisa que um
WHEREdireto emitens_pedidonão conseguiria (lá, cada linha é só um item).
Seis pedidos passaram de R$ 500, e o maior é o pedido 3. Repare como o comando se lê de cima para baixo, na ordem em que você pensaria: "primeiro calculo o total de cada pedido; depois escolho os maiores que 500".
Este exemplo também sai com GROUP BY pedido_id HAVING SUM(…) > 500 (aula HAVING). Com o HAVING, a conta e o filtro ficam misturados no mesmo SELECT. Com a CTE, a conta fica pronta num lugar e você pode usá-la de vários jeitos depois, como nas próximas seções.
Missão: a Ana mudou de ideia: quer os pedidos com valor acima de R$ 400. Troque o número no WHERE do comando principal e rode. Quantos pedidos aparecem?
WITH totais AS (
SELECT pedido_id,
SUM(quantidade * preco_unitario) AS valor
FROM itens_pedido
GROUP BY pedido_id
)
SELECT pedido_id, valor
FROM totais
WHERE valor > 500
ORDER BY valor DESC;
WITH totais AS (
SELECT pedido_id,
SUM(quantidade * preco_unitario) AS valor
FROM itens_pedido
GROUP BY pedido_id
)
SELECT pedido_id, valor
FROM totais
WHERE valor > 400
ORDER BY valor DESC;
3. A mesma CTE usada duas vezes
Agora a pergunta é: quais pedidos ficaram acima da média? Para isso você precisa do total de cada pedido duas vezes: uma para calcular a média e outra para comparar cada pedido com ela. Com a CTE, o rascunho é escrito uma vez e lido duas:
WITH totais AS (
SELECT pedido_id,
ROUND(SUM(quantidade * preco_unitario), 2) AS valor
FROM itens_pedido
GROUP BY pedido_id
)
SELECT pedido_id, valor
FROM totais
WHERE valor > (SELECT AVG(valor) FROM totais)
ORDER BY valor DESC
LIMIT 5;
- ROUND(…, 2)
- Arredonda cada total para 2 casas, como dinheiro (aula Funções Numéricas).
- FROM totais
- Primeira leitura: as linhas que o comando principal vai mostrar.
- (SELECT AVG(valor) FROM totais)
- Segunda leitura, dentro de uma subconsulta: a média dos 90 totais, que dá
205.971111111111(uns R$ 205,97). - LIMIT 5
- Mostra só os 5 maiores, para o quadro não ficar comprido. Sem o
LIMIT, são 37 pedidos acima da média.
Sem a CTE, a conta do total teria de aparecer duas vezes, copiada e colada (você vai ver essa versão na seção 7). Se um dia a regra do total mudar (por exemplo, incluir o frete), com a CTE você muda num lugar só.
4. Várias CTEs, uma depois da outra
Um WITH pode ter várias CTEs, separadas por vírgula. E cada uma pode ler as anteriores, como uma receita em etapas. A Ana quer os 5 clientes que mais gastaram:
totais: o valor de cada pedido (a mesma da seção 2).por_cliente: soma os totais de cada cliente, usandototais.- O comando principal junta com
clientespara mostrar o nome.
WITH totais AS (
SELECT pedido_id,
ROUND(SUM(quantidade * preco_unitario), 2) AS valor
FROM itens_pedido
GROUP BY pedido_id
),
por_cliente AS (
SELECT p.cliente_id,
COUNT(*) AS pedidos,
ROUND(SUM(t.valor), 2) AS gasto
FROM pedidos AS p
JOIN totais AS t ON t.pedido_id = p.id
GROUP BY p.cliente_id
)
SELECT c.nome, pc.pedidos, pc.gasto
FROM por_cliente AS pc
JOIN clientes AS c ON c.id = pc.cliente_id
ORDER BY pc.gasto DESC
LIMIT 5;
- ),
- A vírgula depois do parêntese que fecha
totaisdiz "vem mais uma CTE". Não se repete a palavraWITH. - por_cliente AS (
- A segunda CTE. Só o nome e o
AS, como a primeira. - JOIN totais AS t
- A segunda CTE lê a primeira com um
JOINcomum (aula INNER JOIN). CTE vale em qualquer lugar onde uma tabela valeria. - )
- Depois da última CTE não vai vírgula: vem direto o comando principal.
- JOIN clientes AS c
- O comando principal mistura CTE e tabela de verdade sem problema.
A Conceição Prado lidera, com R$ 1.345,73 em 4 pedidos. Cada CTE resolve uma pergunta pequena, e você pode conferir uma de cada vez: troque o comando principal por SELECT * FROM totais; ou SELECT * FROM por_cliente; para ver só aquele rascunho.
Experimente: descubra o que tem dentro de cada rascunho. Troque o comando principal (as três últimas linhas) por SELECT * FROM por_cliente; e rode. Depois tente SELECT * FROM totais;.
WITH totais AS (
SELECT pedido_id,
ROUND(SUM(quantidade * preco_unitario), 2) AS valor
FROM itens_pedido
GROUP BY pedido_id
),
por_cliente AS (
SELECT p.cliente_id,
COUNT(*) AS pedidos,
ROUND(SUM(t.valor), 2) AS gasto
FROM pedidos AS p
JOIN totais AS t ON t.pedido_id = p.id
GROUP BY p.cliente_id
)
SELECT c.nome, pc.pedidos, pc.gasto
FROM por_cliente AS pc
JOIN clientes AS c ON c.id = pc.cliente_id
ORDER BY pc.gasto DESC;
5. Dando nome às colunas da CTE
Em vez de usar AS em cada coluna lá dentro, você pode escrever a lista de nomes logo depois do nome da CTE, entre parênteses:
WITH totais(pedido, valor) AS ( SELECT pedido_id, SUM(quantidade * preco_unitario) FROM itens_pedido GROUP BY pedido_id ) SELECT pedido, valor FROM totais ORDER BY valor DESC LIMIT 3;
- totais(pedido, valor)
- A CTE se chama
totaise tem duas colunas: a primeira coluna doSELECTde dentro virapedido; a segunda viravalor. Vale a posição, não o nome original. - SUM(quantidade * preco_unitario)
- Aqui não precisou de
AS valor: o nome já veio da lista.
A quantidade de nomes na lista tem de ser igual à quantidade de colunas do SELECT de dentro. Se não for, dá erro (veja em Quando dá errado). Esse jeito de nomear vai ser muito usado na próxima aula, CTE Recursiva.
6. WITH antes de um DELETE
O WITH não serve só para SELECT. No SQLite ele também pode vir antes de INSERT, UPDATE e DELETE. A Ana quer apagar do catálogo os produtos que nunca foram vendidos. Antes de apagar, ela confere quem são, sempre (aula O Perigo do UPDATE e DELETE sem WHERE):
WITH nunca_vendidos AS ( SELECT id FROM produtos WHERE id NOT IN (SELECT produto_id FROM itens_pedido) ) SELECT id, nome FROM produtos WHERE id IN (SELECT id FROM nunca_vendidos);
São os 3 produtos de jardinagem que ninguém comprou. Agora o mesmo rascunho, com um DELETE no lugar do SELECT principal, e uma contagem depois para conferir:
WITH nunca_vendidos AS ( SELECT id FROM produtos WHERE id NOT IN (SELECT produto_id FROM itens_pedido) ) DELETE FROM produtos WHERE id IN (SELECT id FROM nunca_vendidos); SELECT COUNT(*) AS produtos_restantes FROM produtos;
- WITH nunca_vendidos AS (…)
- O mesmo rascunho da conferência, sem mudar nada. É essa a vantagem: o que você conferiu é exatamente o que vai ser apagado.
- DELETE FROM produtos WHERE id IN (…)
- O comando principal agora é um
DELETE(aula DELETE). Ele lê a CTE na subconsulta doWHERE. - SELECT COUNT(*) …
- Um segundo comando, separado por
;. Ele mostra que sobraram 42 produtos dos 45. No editor, cada Rodar começa com o banco limpo: os 45 produtos voltam na próxima vez.
7. Formas diferentes de fazer a mesma coisa
A pergunta da seção 3 ("pedidos acima da média") pode ser resolvida de quatro jeitos. Todos dão o mesmo resultado; o que muda é a leitura e quanto tempo o rascunho dura.
a) Com subconsulta no FROM
É a forma da aula Subconsultas. Repare que a conta do total aparece duas vezes:
SELECT pedido_id, valor
FROM (SELECT pedido_id,
ROUND(SUM(quantidade * preco_unitario), 2) AS valor
FROM itens_pedido
GROUP BY pedido_id) AS totais
WHERE valor > (SELECT AVG(valor)
FROM (SELECT pedido_id,
ROUND(SUM(quantidade * preco_unitario), 2) AS valor
FROM itens_pedido
GROUP BY pedido_id))
ORDER BY valor DESC
LIMIT 5;
b) Com CTE
É o exemplo da seção 3: a conta aparece uma vez, no alto, com nome.
c) Com uma view
Uma view (aula Views) é uma consulta com nome guardada no banco: ela continua lá amanhã, para qualquer comando usar. A CTE é a versão descartável, que vale só para um comando.
CREATE VIEW totais AS
SELECT pedido_id,
ROUND(SUM(quantidade * preco_unitario), 2) AS valor
FROM itens_pedido
GROUP BY pedido_id;
SELECT pedido_id, valor
FROM totais
WHERE valor > (SELECT AVG(valor) FROM totais)
ORDER BY valor DESC
LIMIT 5;
d) Com uma tabela temporária
Uma tabela temporária guarda o resultado calculado até você fechar a conexão com o banco (no editor, até o fim do Rodar). Serve quando o rascunho é pesado e vai ser lido em vários comandos seguidos:
CREATE TEMP TABLE totais AS
SELECT pedido_id,
ROUND(SUM(quantidade * preco_unitario), 2) AS valor
FROM itens_pedido
GROUP BY pedido_id;
SELECT pedido_id, valor
FROM totais
WHERE valor > (SELECT AVG(valor) FROM totais)
ORDER BY valor DESC
LIMIT 5;
| Forma | Quanto tempo dura | Quando usar |
|---|---|---|
Subconsulta no FROM | Só aquele trecho do comando | Rascunho pequeno, usado uma vez |
CTE (WITH) | Um comando | Rascunho usado mais de uma vez no mesmo comando, ou consulta em etapas que você quer ler de cima para baixo |
View (CREATE VIEW) | Para sempre, até um DROP VIEW | Uma consulta que você (ou outros) vão usar todo dia |
Tabela temporária (CREATE TEMP TABLE … AS) | Até fechar a conexão | Um resultado pesado de calcular, lido por vários comandos seguidos |
Quando dá errado
As mensagens abaixo são as reais do SQLite, em inglês, como aparecem no editor. No programa sqlite3, as mesmas mensagens saem com o prefixo Parse error: na frente (por exemplo, Parse error: no such table: caros).
A Ana terminou a CTE com ;, achando que era um comando à parte:
WITH caros AS ( SELECT nome, preco FROM produtos WHERE preco > 100 ); SELECT nome FROM caros;
A mensagem: near ";": syntax error, "perto do ;: erro de sintaxe". Uma CTE não é um comando sozinho: ela é o começo de um comando. O SQLite leu WITH caros AS (…), esperava o SELECT principal e encontrou o fim do comando.
Como consertar: tire o ; depois do parêntese. O único ; vai no fim do SELECT principal.
O primeiro comando funciona. O segundo tenta usar a CTE do primeiro:
WITH caros AS ( SELECT nome, preco FROM produtos WHERE preco > 100 ) SELECT COUNT(*) AS quantos FROM caros; SELECT nome FROM caros;
A mensagem: no such table: caros, "tabela inexistente: caros". O quadro mostra o resultado do primeiro comando (4) e para no segundo. O rascunho morreu no ; do primeiro comando.
Como consertar: repita o WITH no segundo comando, ou, se o rascunho vai ser usado em muitos comandos, transforme-o numa view ou numa tabela temporária (seção 7).
Vírgula depois da última CTE:
WITH caros AS ( SELECT nome, preco FROM produtos WHERE preco > 100 ), SELECT nome FROM caros;
A mensagem: near "SELECT": syntax error. A vírgula avisou "vem mais uma CTE", e o SQLite esperava um nome de CTE; veio um SELECT.
E o contrário, sem a vírgula entre duas CTEs:
WITH caros AS ( SELECT nome, preco FROM produtos WHERE preco > 100 ) baratos AS ( SELECT nome, preco FROM produtos WHERE preco < 5 ) SELECT nome FROM caros;
A mensagem: near "baratos": syntax error. Sem a vírgula, o SQLite esperava o comando principal depois do ) e encontrou um nome solto.
Como consertar: vírgula entre as CTEs, nenhuma depois da última.
WITH caros AS ( SELECT nome FROM produtos WHERE preco > 100 ) SELECT nome, preco FROM caros;
A mensagem: no such column: preco, "coluna inexistente: preco". A tabela produtos tem preco, mas a CTE caros só tem as colunas que o SELECT de dentro devolveu: aqui, só nome.
Como consertar: coloque a coluna no SELECT de dentro da CTE: SELECT nome, preco FROM produtos ….
WITH totais(pedido, valor) AS ( SELECT pedido_id FROM itens_pedido ) SELECT * FROM totais;
A mensagem: table totais has 1 values for 2 columns, "a tabela totais tem 1 valor para 2 colunas". A lista prometeu duas colunas, mas o SELECT de dentro só devolve uma.
Como consertar: o número de nomes na lista tem de ser igual ao número de colunas do SELECT: tire o valor da lista ou acrescente a conta no SELECT.
WITH caros AS ( SELECT nome FROM produtos WHERE preco > 100 ), caros AS ( SELECT nome FROM produtos WHERE preco > 200 ) SELECT nome FROM caros;
A mensagem: duplicate WITH table name: caros, "nome de tabela do WITH repetido: caros".
Como consertar: cada CTE do mesmo WITH precisa de um nome diferente, por exemplo caros e muito_caros.
Uma CTE pode ter o mesmo nome de uma tabela de verdade. Nesse caso, dentro daquele comando, a CTE ganha:
WITH produtos AS ( SELECT 'só o rascunho' AS nome ) SELECT nome FROM produtos;
Em vez dos 45 produtos, veio uma linha só. Nenhum erro avisa: o comando "esconde" a tabela produtos atrás do rascunho. Escolha nomes de CTE que não sejam nomes de tabelas, como totais, caros, por_cliente.
- MySQL tem CTE a partir da versão 8.0. Lá, uma CTE só pode usar as que vieram antes dela no mesmo
WITH. O SQLite é mais tolerante e aceita uma CTE que usa outra escrita depois; não conte com isso, porque o mesmo comando falha no MySQL e no SQL Server. Escreva sempre na ordem. - SQL Server: se o
WITHvier depois de outro comando no mesmo lote, o comando anterior tem de terminar com;. Por isso você vai ver muito código de SQL Server escrito;WITH totais AS (…). Ele também não aceitaORDER BYdentro da CTE (a não ser junto comTOPouOFFSET). - PostgreSQL permite
INSERT,UPDATEeDELETEdentro da CTE (comRETURNING), coisa que o SQLite não faz. O PostgreSQL e o SQLite (desde a versão 3.35) aceitam ainda as dicasAS MATERIALIZEDeAS NOT MATERIALIZED, que pedem para o banco calcular o rascunho uma vez só ou misturá-lo ao comando principal.
Trecho só ilustrativo, do SQL Server (não roda no SQLite):
SELECT 1; ;WITH totais AS ( SELECT pedido_id, SUM(quantidade * preco_unitario) AS valor FROM itens_pedido GROUP BY pedido_id ) SELECT TOP 3 pedido_id, valor FROM totais ORDER BY valor DESC;
Referência rápida
| Escrita | O que faz |
|---|---|
WITH nome AS (SELECT …) SELECT … FROM nome; | Cria uma CTE e a usa no comando principal |
WITH a AS (…), b AS (…) SELECT … | Várias CTEs, separadas por vírgula; b pode ler a |
WITH nome(col1, col2) AS (…) | Dá nome às colunas pela posição |
… WHERE x > (SELECT AVG(x) FROM nome) | Lê a mesma CTE de novo, numa subconsulta |
WITH nome AS (…) DELETE FROM t WHERE id IN (SELECT id FROM nome); | CTE antes de DELETE (vale também para INSERT e UPDATE) |
WITH nome AS MATERIALIZED (…) | Dica para calcular o rascunho uma vez só (SQLite 3.35+) |
| Mensagem | Quer dizer |
|---|---|
near ";": syntax error | Um ; depois da CTE, antes do comando principal |
near "SELECT": syntax error | Vírgula sobrando depois da última CTE |
no such table: caros | A CTE foi usada fora do comando onde foi criada |
no such column: preco | A CTE não devolve essa coluna |
table totais has 1 values for 2 columns | A lista de colunas não bate com o SELECT |
duplicate WITH table name: caros | Duas CTEs com o mesmo nome |
Exercícios
1. A palavra que abre
Complete a palavra que começa uma CTE:
2. Duas CTEs
Complete o sinal que separa as duas CTEs e a palavra que liga o nome da segunda à sua consulta:
3. A Ana criou uma CTE totais num comando e, no comando seguinte, escreveu SELECT * FROM totais;. O que acontece?
4. O SQLite respondeu table resumo has 2 values for 3 columns. Qual é o problema?
5. Uma consulta que toda a equipe da loja vai usar todo dia, durante meses. Qual é a melhor escolha?
Missão final: vendas por categoria
Missão: a CTE vendas já calcula quanto cada categoria vendeu. Escreva o comando principal: junte vendas com categorias e mostre só as categorias que venderam mais de R$ 4.000, com as colunas categoria (o nome da categoria) e vendido.
WITH vendas AS (
SELECT pr.categoria_id,
ROUND(SUM(i.quantidade * i.preco_unitario), 2) AS vendido
FROM itens_pedido AS i
JOIN produtos AS pr ON pr.id = i.produto_id
GROUP BY pr.categoria_id
)
SELECT * FROM vendas;
WITH vendas AS (
SELECT pr.categoria_id,
ROUND(SUM(i.quantidade * i.preco_unitario), 2) AS vendido
FROM itens_pedido AS i
JOIN produtos AS pr ON pr.id = i.produto_id
GROUP BY pr.categoria_id
)
SELECT c.nome AS categoria, v.vendido
FROM vendas AS v
JOIN categorias AS c ON c.id = v.categoria_id
WHERE v.vendido > 4000
ORDER BY v.vendido DESC;
Eletrônicos e Casa e Cozinha são as duas categorias que passaram de R$ 4.000. Repare que você não mexeu na CTE: o rascunho estava certo, só faltava a conta principal.
Resumo
- Uma CTE é uma consulta com nome, escrita com
WITH nome AS ( SELECT … )antes do comando principal. É o rascunho na margem do caderno. - Ela vale só durante um comando: no
;, some. Fora dele, dáno such table. - O comando principal usa a CTE como uma tabela: em
FROM,JOIN, subconsultas, e quantas vezes precisar. - Várias CTEs vão separadas por vírgula, sem repetir o
WITH; cada uma pode ler as anteriores. Nenhuma vírgula depois da última, nenhum;antes do comando principal. WITH nome(a, b) AS (…)dá nome às colunas pela posição; a quantidade tem de bater.- Para um rascunho de um comando, CTE; para todo dia, view; para um resultado pesado usado em vários comandos, tabela temporária.
- Próxima aula: a CTE que lê a si mesma, para contar, gerar datas e percorrer a hierarquia dos funcionários: CTE Recursiva.