Guida pratica alla clausola APPLY / CROSS APPLY / OUTER APPLY (con esempi)

SQL Server, una delle più potenti piattaforme di gestione dei database relazionali, offre una vasta gamma di funzionalità per la manipolazione e l’interrogazione dei dati. Tra queste, la clausola APPLY (nelle sue varianti CROSS APPLY e OUTER APPLY) rappresenta uno strumento estremamente versatile e, a volte, poco compreso, ma fondamentale per risolvere scenari complessi di join e correlazione tra tabelle. Questo articolo è progettato per demistificare CROSS APPLY, esplorando il suo funzionamento, i suoi casi d’uso principali e come si distingue dalle operazioni di join più tradizionali.

Cos’è CROSS APPLY?

Immagina di avere una tabella principale e di voler eseguire una funzione a valori di tabella (TVF – Table-Valued Function) o una subquery su ogni singola riga della tabella principale, e poi unire i risultati. È esattamente qui che entra in gioco CROSS APPLY.

Tecnicamente, CROSS APPLY agisce come un operatore di join correlato. La sua sintassi è:

SQL

SELECT
    T1.ColonnaA,
    T1.ColonnaB,
    T2.ColonnaC
FROM
    Tabella1 AS T1
CROSS APPLY
    (SELECT ... FROM Tabella2 WHERE T2.ID = T1.ID) AS T2; -- Qui la subquery o TVF

Per ogni riga della Tabella1 (la tabella “esterna”), la subquery o la TVF specificata dopo CROSS APPLY viene eseguita. Se questa subquery/TVF restituisce delle righe, queste vengono unite (come un INNER JOIN) alla riga corrente della Tabella1. Se la subquery/TVF non restituisce alcuna riga per una data riga della Tabella1, quella riga della Tabella1 non apparirà nel risultato finale.

Punti chiave:

  • Correlazione: la query all’interno di APPLY può referenziare colonne della tabella “esterna” (quella prima di APPLY).
  • Esecuzione riga per riga: la query all’interno di APPLY viene logicamente eseguita per ogni riga della tabella esterna.
  • Comportamento da INNER JOIN: se la subquery/TVF non produce risultati per una riga esterna, quella riga viene esclusa.

Quando Usare CROSS APPLY? Casi d’Uso Comuni

CROSS APPLY brilla in scenari dove un semplice JOIN non è sufficiente o risulta inefficiente. Ecco alcuni dei casi d’uso più frequenti:

1. Funzioni a Valori di Tabella (TVF)

Questo è l’esempio più diretto. Se hai una TVF che richiede un parametro da una colonna della tua tabella principale, CROSS APPLY è la soluzione perfetta.

Scenario: Hai una tabella Ordini e una TVF dbo.GetDettaglioOrdine(@OrderID) che restituisce i prodotti di un ordine.

SQL

-- Esempio teorico di una TVF (non eseguibile così com'è)
/*
CREATE FUNCTION dbo.GetDettaglioOrdine (@OrderID INT)
RETURNS TABLE
AS
RETURN
(
    SELECT Prodotto, Quantita, Prezzo
    FROM DettagliOrdine
    WHERE OrderID = @OrderID
);
*/

-- Utilizzo di CROSS APPLY con la TVF
SELECT
    o.OrderID,
    o.DataOrdine,
    d.Prodotto,
    d.Quantita,
    d.Prezzo
FROM
    Ordini AS o
CROSS APPLY
    dbo.GetDettaglioOrdine(o.OrderID) AS d;

Per ogni ordine, la funzione GetDettaglioOrdine viene chiamata con l’ID dell’ordine, e i risultati vengono uniti.

2. Trovare i “Top N” per Gruppo

Questo è un classico problema dove CROSS APPLY offre una soluzione elegante e spesso performante. Vuoi trovare, ad esempio, i 3 prodotti più venduti per ogni categoria.

Scenario: Hai una tabella Prodotti e una tabella Vendite. Vuoi i 2 prodotti più venduti per ogni categoria di prodotto.

SQL

-- Dati di esempio
CREATE TABLE Categorie (
    CategoriaID INT PRIMARY KEY,
    NomeCategoria VARCHAR(50)
);

CREATE TABLE Prodotti (
    ProdottoID INT PRIMARY KEY,
    NomeProdotto VARCHAR(50),
    CategoriaID INT REFERENCES Categorie(CategoriaID)
);

CREATE TABLE Vendite (
    VenditaID INT PRIMARY KEY,
    ProdottoID INT REFERENCES Prodotti(ProdottoID),
    QuantitaVenduta INT
);

INSERT INTO Categorie VALUES (1, 'Elettronica'), (2, 'Abbigliamento'), (3, 'Libri');
INSERT INTO Prodotti VALUES (101, 'Smartphone', 1), (102, 'Laptop', 1), (103, 'Cuffie', 1),
                            (201, 'Maglietta', 2), (202, 'Pantaloni', 2), (203, 'Cappello', 2),
                            (301, 'Romanzo', 3), (302, 'Saggio', 3), (303, 'Fumetto', 3);
INSERT INTO Vendite VALUES (1, 101, 50), (2, 102, 30), (3, 103, 40), (4, 101, 60),
                           (5, 201, 100), (6, 202, 80), (7, 203, 90),
                           (8, 301, 20), (9, 302, 25), (10, 303, 15);

-- La query con CROSS APPLY
SELECT
    c.NomeCategoria,
    p.NomeProdotto,
    tv.TotaleVenduto
FROM
    Categorie AS c
CROSS APPLY
    (
        SELECT TOP 2
            p_inner.NomeProdotto,
            SUM(v.QuantitaVenduta) AS TotaleVenduto
        FROM
            Prodotti AS p_inner
        JOIN
            Vendite AS v ON p_inner.ProdottoID = v.ProdottoID
        WHERE
            p_inner.CategoriaID = c.CategoriaID -- Correlazione qui!
        GROUP BY
            p_inner.NomeProdotto
        ORDER BY
            TotaleVenduto DESC
    ) AS tv
JOIN
    Prodotti AS p ON p.NomeProdotto = tv.NomeProdotto AND p.CategoriaID = c.CategoriaID; -- Unione finale per ottenere i dettagli del prodotto

-- Pulisco i dati di test
DROP TABLE Vendite;
DROP TABLE Prodotti;
DROP TABLE Categorie;

In questo esempio, per ogni categoria, la subquery CROSS APPLY seleziona i 2 prodotti più venduti di quella specifica categoria.

3. Scomposizione di Stringhe o XML/JSON

Se hai una colonna che contiene dati complessi (es. una stringa con valori separati da virgole, un XML o un JSON) e devi “esploderli” in righe separate, CROSS APPLY è lo strumento ideale con le funzioni appropriate (es. STRING_SPLIT, OPENJSON, OPENXML).

Scenario: Hai una tabella di articoli con una colonna Tags che è una stringa delimitata da virgole. Vuoi ottenere ogni tag su una riga separata.

SQL

CREATE TABLE Articoli (
    ArticoloID INT PRIMARY KEY,
    Titolo VARCHAR(100),
    Tags VARCHAR(200)
);

INSERT INTO Articoli VALUES
(1, 'Guida SQL Server', 'SQL,Database,Performance'),
(2, 'Concetti di .NET', 'C#,OOP,Framework'),
(3, 'Web Development', 'HTML,CSS,JavaScript');

SELECT
    a.Titolo,
    t.value AS Tag
FROM
    Articoli AS a
CROSS APPLY
    STRING_SPLIT(a.Tags, ',') AS t;

-- Pulisco i dati di test
DROP TABLE Articoli;

Qui, STRING_SPLIT (disponibile da SQL Server 2016) viene applicato per ogni riga della tabella Articoli, scomponendo la stringa Tags in singole righe.

CROSS APPLY vs. INNER JOIN

La differenza principale sta nella dipendenza della query secondaria dalla query principale.

  • INNER JOIN: Le tabelle vengono unite in base a una condizione di join. Le operazioni di filtro o le subquery nella clausola ON o WHERE vengono valutate in base a valori che sono già stati determinati dall’unione delle tabelle. Le subquery in un JOIN sono tipicamente non correlate (o correlate solo a una singola tabella all’interno della JOIN stessa).

  • CROSS APPLY: La subquery o la TVF sul lato destro di APPLY viene eseguita per ogni riga della tabella sinistra. Può, e spesso lo fa, fare riferimento a colonne della tabella di sinistra (la tabella “esterna”). Questo permette una logica “riga per riga” che un semplice INNER JOIN non può fornire direttamente per le subquery.

In molti casi, una query con CROSS APPLY può essere riscritta usando una CTE (Common Table Expression) con ROW_NUMBER() o subquery complesse, ma CROSS APPLY spesso rende il codice più leggibile e, in alcuni scenari, l’ottimizzatore di query può trovare un piano di esecuzione più efficiente.

CROSS APPLY vs. OUTER APPLY

Abbiamo parlato finora di CROSS APPLY che si comporta come un INNER JOIN, escludendo le righe della tabella esterna se la subquery/TVF non produce risultati.

Esiste anche OUTER APPLY, che si comporta come un LEFT OUTER JOIN. Se la subquery/TVF non restituisce righe per una determinata riga della tabella esterna, quella riga della tabella esterna viene comunque inclusa nel risultato, con i valori delle colonne della subquery/TVF impostati a NULL.

Quando usare OUTER APPLY?

Quando vuoi vedere tutte le righe della tabella esterna, anche se non c’è una corrispondenza nella subquery o nella TVF.

CROSS APPLY è uno strumento potente e flessibile in SQL Server che ogni sviluppatore dovrebbe padroneggiare. Permette di risolvere scenari complessi come il “top N per gruppo”, l’interrogazione di TVF e la scomposizione di stringhe o dati semi-strutturati, offrendo una sintassi chiara e prestazioni spesso superiori rispetto a soluzioni alternative più tortuose.

Se ti trovi a scrivere subquery correlate che si ripetono o a lottare con la logica “per ogni riga”, pensa a CROSS APPLY. Potrebbe essere esattamente la soluzione che stai cercando.