Una subquery è una query annidata dentro un'altra query. Una CTE (common table expression) è la stessa query interna spostata in cima allo statement con WITH e a cui viene dato un nome, così una query lunga si legge come una serie di passaggi. Gli esempi funzionano in PostgreSQL, MySQL, SQLite e SQL Server.
Come usare CTE e subquery
Una subquery si scrive dove viene usata: in FROM fa da tabella, mentre in WHERE o SELECT fornisce un valore o una lista. Una CTE si definisce prima e poi la query che segue la usa come una tabella:
-- Subquery: written where it's used
SELECT columns FROM (SELECT ...) AS name;
-- CTE: named first, then used like a table
WITH name AS (SELECT ...)
SELECT columns FROM name;
SELECT n * 2 AS doubled FROM (SELECT 21 AS n) AS numbers;
-- Output: 42
WITH numbers AS (SELECT 21 AS n)
SELECT n * 2 AS doubled FROM numbers;
-- Output: 42
Su una tabella reale le due forme restituiscono le stesse righe. Ecco lo stesso filtro scritto in entrambi i modi:
-- Input: people
-- first_name | last_name | job | favorite_food
-- -----------+-----------+----------+--------------
-- Matthew | Ortiz | Chef | cheese
-- Matt | Novak | Pilot | pizza
-- Mattie | Brooks | Engineer | cheese
-- Priya | Nair | Teacher | cheese
-- Subquery
SELECT *
FROM (SELECT first_name, last_name, job FROM people WHERE favorite_food = 'cheese') AS cheese_lovers
WHERE first_name LIKE 'Matt%';
-- CTE
WITH cheese_lovers AS (
SELECT first_name, last_name, job FROM people WHERE favorite_food = 'cheese'
)
SELECT * FROM cheese_lovers
WHERE first_name LIKE 'Matt%';
-- Output of both:
-- first_name | last_name | job
-- -----------+-----------+---------
-- Matthew | Ortiz | Chef
-- Mattie | Brooks | Engineer
Regole da conoscere
-
Una CTE vale per un solo statement. La query subito dopo
WITHpuò usarla; lo statement successivo no. Per conservare un risultato per più query, usa una tabella temporanea o una vista. -
Dai un nome a una subquery in
FROM, comeAS numbersqui sopra. MySQL e SQL Server la rifiutano se manca. - Una subquery usata come valore singolo deve restituire una sola riga:
SELECT (SELECT 1 UNION ALL SELECT 2) AS n;
-- ERROR: more than one row returned by a subquery used as an expression
-- (SQLite doesn't complain: it quietly uses the first row, 1)
- Di solito la velocità è la stessa. In genere i database pianificano una CTE usata una sola volta come la subquery equivalente, quindi scegli quella più leggibile.
Sintassi alternativa
Una CTE che fa riferimento a se stessa (vedi sotto) si scrive WITH RECURSIVE:
-- In SQL Server, leave out RECURSIVE
WITH RECURSIVE counter AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM counter WHERE n < 3
)
SELECT n FROM counter;
-- Output:
-- n
-- -
-- 1
-- 2
-- 3
In SQL Server, lo statement prima di un WITH deve terminare con un punto e virgola: ecco perché nel codice T-SQL una CTE spesso inizia con ;WITH.
Quando usare una CTE invece di una subquery?
Una subquery va bene per un singolo piccolo passaggio. Passa a una CTE quando la query ha più passaggi, le serve lo stesso risultato due volte o deve fare riferimento a se stessa.
Filtrare rispetto a un valore calcolato
Quando la query interna produce solo un valore o una lista con cui confrontare, come il totale medio degli ordini, una subquery in WHERE è il modo più breve per scriverla. Funzionerebbe anche una CTE, ma aggiungerebbe un nome che non serve a nessuno.
Dividere una query lunga in passaggi con un nome
Quando una query ha più fasi (totale per cliente, poi i primi della lista, poi l'elenco finale), dai un nome a ogni fase. Elenca più CTE dopo un solo WITH, separate da virgole: ognuna può usare quelle precedenti, e la query si legge dall'alto in basso invece che dall'interno verso l'esterno.
Usare lo stesso risultato più volte
Uno statement può fare riferimento a una CTE tutte le volte che vuole, quindi un risultato che ti serve due volte (una lista di totali e la loro media, o ogni totale e il totale complessivo) si scrive una volta sola. Con le subquery dovresti copiare l'intera query interna.
Percorrere una gerarchia
Solo una CTE può fare riferimento a se stessa. Una CTE ricorsiva parte da alcune righe e continua a fare join con la tabella finché non compaiono più righe nuove: è così che percorri un organigramma o un albero di categorie, o generi una serie di numeri o date.
Esempi reali di CTE e subquery
Prodotti con prezzo superiore alla media della categoria
La subquery fa riferimento a p.category della query esterna, quindi viene calcolata per la categoria di ciascun prodotto (una subquery correlata):
-- Input: products
-- name | category | price
-- --------------+-----------+-------
-- Desk Lamp | Lighting | 24.99
-- Floor Lamp | Lighting | 89.00
-- Office Chair | Furniture | 149.00
-- Standing Desk | Furniture | 399.00
-- Bookshelf | Furniture | 120.00
SELECT name, category, price
FROM products p
WHERE price > (
SELECT AVG(price) FROM products WHERE category = p.category
)
ORDER BY category, name;
-- Output:
-- name | category | price
-- --------------+-----------+-------
-- Standing Desk | Furniture | 399.00
-- Floor Lamp | Lighting | 89.00
Il prodotto più venduto di ogni categoria
Tre passaggi, ognuno con un nome: somma le unità per prodotto, ordina i prodotti all'interno di ogni categoria, poi tieni il primo:
-- Input: sales
-- id | product | category | units
-- ---+--------------+-----------+------
-- 1 | Desk Lamp | Lighting | 3
-- 2 | Floor Lamp | Lighting | 5
-- 3 | Desk Lamp | Lighting | 4
-- 4 | Office Chair | Furniture | 2
-- 5 | Bookshelf | Furniture | 6
WITH product_totals AS (
SELECT category, product, SUM(units) AS units
FROM sales
GROUP BY category, product
),
ranked AS (
SELECT category, product, units,
RANK() OVER (PARTITION BY category ORDER BY units DESC) AS place
FROM product_totals
)
SELECT category, product, units
FROM ranked
WHERE place = 1
ORDER BY category;
-- Output:
-- category | product | units
-- ----------+-----------+------
-- Furniture | Bookshelf | 6
-- Lighting | Desk Lamp | 7
La quota di vendite di ogni regione
region_totals viene usata due volte: una per le righe e una dentro una subquery per il totale complessivo:
-- Input: orders
-- id | region | amount
-- ---+--------+--------
-- 1 | North | 1200.00
-- 2 | South | 450.00
-- 3 | North | 300.00
-- 4 | West | 800.00
-- 5 | South | 250.00
WITH region_totals AS (
SELECT region, SUM(amount) AS total
FROM orders
GROUP BY region
)
SELECT
region,
total,
ROUND(100.0 * total / (SELECT SUM(total) FROM region_totals), 1) AS pct_of_sales
FROM region_totals
ORDER BY total DESC;
-- Output:
-- region | total | pct_of_sales
-- -------+---------+-------------
-- North | 1500.00 | 50.0
-- West | 800.00 | 26.7
-- South | 700.00 | 23.3
Scritta con le subquery, la query con GROUP BY comparirebbe due volte.
La catena di manager di un dipendente
Parti da un dipendente e segui manager_id verso l'alto finché non si esaurisce:
-- Input: employees
-- id | name | manager_id
-- ---+-----------+-----------
-- 1 | Maya Chen | NULL
-- 2 | Sam Ortiz | 1
-- 3 | Jo Kim | 2
-- 4 | Lee Ross | 2
-- 5 | Ana Diaz | 1
-- In SQL Server, leave out RECURSIVE
WITH RECURSIVE chain AS (
SELECT id, name, manager_id, 0 AS level
FROM employees
WHERE name = 'Jo Kim'
UNION ALL
SELECT e.id, e.name, e.manager_id, chain.level + 1
FROM employees e
JOIN chain ON e.id = chain.manager_id
)
SELECT level, name FROM chain ORDER BY level;
-- Output:
-- level | name
-- ------+----------
-- 0 | Jo Kim
-- 1 | Sam Ortiz
-- 2 | Maya Chen
Altre letture su CTE e subquery
Su questo blog:
- Guida alle tabelle temporanee SQL: conservare un risultato intermedio per più di uno statement
-
Guida per trovare i duplicati in SQL: CTE con
ROW_NUMBER()per individuare le righe ripetute - Guida per rimuovere i duplicati in SQL: eliminare i duplicati tramite una CTE
- Guida a GROUP BY in SQL: le aggregazioni su cui si basano la maggior parte delle CTE
- Guida ai JOIN SQL con diagrammi: fare join tra una CTE e altre tabelle
Documentazione ufficiale:
- PostgreSQL: query WITH ed espressioni subquery
- MySQL: WITH (common table expression) e subquery
- SQLite: la clausola WITH
- SQL Server: WITH common_table_expression e subquery
Beekeeper Studio è una GUI per database gratuita & open source
Il miglior strumento per query & editor SQL che abbia mai usato. Offre tutto ciò che mi serve per gestire il mio database. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio è veloce, intuitivo e facile da usare. Beekeeper supporta tantissimi database e funziona alla grande su Windows, Mac e Linux.
Cosa dicono gli utenti di Beekeeper Studio
"Beekeeper Studio ha sostituito completamente il mio vecchio workflow SQL. È veloce, intuitivo e rende di nuovo piacevole lavorare con i database."
"Ho provato molte GUI per database, ma Beekeeper trova il perfetto equilibrio tra funzionalità e semplicità. Funziona e basta."