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.

Você vai aprender
  • O que é uma CTE e como escrever o WITH nome AS (…).
  • Como usar a CTE no SELECT principal, em WHERE, em JOIN e 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 (…).
  • WITH antes de um DELETE.
  • 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 cadernoNo SQL
"Rascunho: subtotal = …"WITH subtotal AS ( SELECT … )
A conta principal, que usa o subtotalo SELECT principal, que lê FROM subtotal
Virou a página, o rascunho não vale maisterminou 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 SELECT comum, como os que você já escreve. As colunas que ele devolve (nome e preco) 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ê caros como 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 WHERE direto em itens_pedido nã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".

E o HAVING?

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:

  1. totais: o valor de cada pedido (a mesma da seção 2).
  2. por_cliente: soma os totais de cada cliente, usando totais.
  3. O comando principal junta com clientes para 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 totais diz "vem mais uma CTE". Não se repete a palavra WITH.
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 JOIN comum (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 totais e tem duas colunas: a primeira coluna do SELECT de dentro vira pedido; a segunda vira valor. 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 do WHERE.
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;
FormaQuanto tempo duraQuando usar
Subconsulta no FROMSó aquele trecho do comandoRascunho pequeno, usado uma vez
CTE (WITH)Um comandoRascunho 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 VIEWUma consulta que você (ou outros) vão usar todo dia
Tabela temporária (CREATE TEMP TABLE … AS)Até fechar a conexãoUm 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).

Ponto e vírgula depois da CTE

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.

A CTE não existe no comando seguinte

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 sobrando ou faltando entre as CTEs

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.

Coluna que a CTE não tem
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 ….

Lista de colunas com a quantidade errada
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.

Duas CTEs com o mesmo nome
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.

Sem erro, mas com surpresa: a CTE com nome de tabela

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.

Nos outros bancos
  • 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 WITH vier 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 aceita ORDER BY dentro da CTE (a não ser junto com TOP ou OFFSET).
  • PostgreSQL permite INSERT, UPDATE e DELETE dentro da CTE (com RETURNING), coisa que o SQLite não faz. O PostgreSQL e o SQLite (desde a versão 3.35) aceitam ainda as dicas AS MATERIALIZED e AS 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

EscritaO 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+)
MensagemQuer dizer
near ";": syntax errorUm ; depois da CTE, antes do comando principal
near "SELECT": syntax errorVírgula sobrando depois da última CTE
no such table: carosA CTE foi usada fora do comando onde foi criada
no such column: precoA CTE não devolve essa coluna
table totais has 1 values for 2 columnsA lista de colunas não bate com o SELECT
duplicate WITH table name: carosDuas CTEs com o mesmo nome

Exercícios

1. A palavra que abre

Complete a palavra que começa uma CTE:

baratos AS (SELECT nome FROM produtos WHERE preco < 5) SELECT nome FROM baratos;

2. Duas CTEs

Complete o sinal que separa as duas CTEs e a palavra que liga o nome da segunda à sua consulta:

WITH caros AS (SELECT id FROM produtos WHERE preco > 100) vendidos (SELECT DISTINCT produto_id FROM itens_pedido) SELECT COUNT(*) FROM caros WHERE id IN (SELECT produto_id FROM vendidos);

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.