CROSS APPLY et OUTER APPLY en T‑SQL

APPLY est un join latéral : pour chaque ligne de la table de gauche, on exécute une expression qui renvoie une table (fonction table, OPENJSON, STRING_SPLIT, sous‑requête corrélée). CROSS APPLY se comporte comme un inner join (exclut les lignes sans résultat), tandis qu’OUTER APPLY conserve la ligne de gauche même si l’expression ne renvoie rien.

Cas d’usage

Dépivoter un CSV par ligne (STRING_SPLIT) avec CROSS APPLY

Quand la fonction renvoie 0 ligne (NULL/chaîne vide), la ligne de gauche disparaît — pratique pour filtrer les entrées vides.

-- Sample data
CREATE TABLE dbo.Customers (
  CustomerId INT PRIMARY KEY,
  Name NVARCHAR(50),
  TagsCsv NVARCHAR(200)
);

INSERT INTO dbo.Customers (CustomerId, Name, TagsCsv) VALUES
  (1, 'Alice', 'vip,tech'),
  (2, 'Bob',   NULL),
  (3, 'Cara',  '');

-- Join each row to a TVF result: split a CSV column into rows per customer
-- CROSS APPLY behaves like an inner join (filters out NULL/empty results)
SELECT
  C.CustomerId,
  C.Name,
  TAG.value AS Tag
FROM dbo.Customers AS C
CROSS APPLY STRING_SPLIT(C.TagsCsv, ',') AS TAG;
Résultat attendu
CustomerId  Name   Tag
----------  -----  -----
1           Alice  vip
1           Alice  tech
Sélectionner le TOP 1 par groupe via OUTER APPLY

Calculer par ligne une sous‑requête ordonnée (ex: l’article le plus commandé) tout en conservant les lignes sans détails.

-- Sample data
CREATE TABLE dbo.Orders (
  OrderId INT PRIMARY KEY,
  CustomerId INT
);
CREATE TABLE dbo.Products (
  Id INT PRIMARY KEY,
  Name NVARCHAR(50)
);
CREATE TABLE dbo.OrderLines (
  OrderId INT,
  ProductId INT,
  Quantity INT
);

INSERT INTO dbo.Orders (OrderId, CustomerId) VALUES (101, 1), (102, 2);
INSERT INTO dbo.Products (Id, Name) VALUES (10, 'Keyboard'), (20, 'Mouse');
INSERT INTO dbo.OrderLines (OrderId, ProductId, Quantity) VALUES
  (101, 10, 1),
  (101, 20, 3);
-- Order 102 has no lines

-- Keep outer rows even when the function returns no rows
SELECT
  O.OrderId,
  O.CustomerId,
  X.TopItemId,
  X.TopItemName,
  X.Qty
FROM dbo.Orders AS O
OUTER APPLY (
  SELECT TOP (1)
         OL.ProductId     AS TopItemId,
         P.Name           AS TopItemName,
         OL.Quantity      AS Qty
  FROM dbo.OrderLines AS OL
  JOIN dbo.Products AS P ON P.Id = OL.ProductId
  WHERE OL.OrderId = O.OrderId
  ORDER BY OL.Quantity DESC
) AS X;
Résultat attendu
OrderId  CustomerId  TopItemId  TopItemName  Qty
-------  ----------  ---------  -----------  ---
101      1           20         Mouse        3
102      2           NULL       NULL         NULL
Parser du JSON par ligne avec OPENJSON

Analyser un payload JSON stocké en colonne et l’exploser en (clé, valeur) sans perdre les enregistrements mal formés (OUTER APPLY).

-- Sample data
CREATE TABLE dbo.RawEvents (
  Id INT PRIMARY KEY,
  Payload NVARCHAR(MAX)
);
INSERT INTO dbo.RawEvents(Id, Payload) VALUES
  (1, '{"country":"FR","score":42}'),
  (2, NULL);

-- Parse JSON per row; OUTER APPLY to keep rows with invalid/missing JSON
SELECT
  T.Id,
  J.[key] AS Prop,
  J.value AS Val
FROM dbo.RawEvents AS T
OUTER APPLY OPENJSON(T.Payload) AS J;
Résultat attendu
Id  Prop     Val
--  -------  ---
1   country  FR
1   score    42
2   NULL     NULL

Alternatives

Fonctions de fenêtre (OVER / PARTITION BY)

Pour le TOP 1 par groupe, on peut utiliser ROW_NUMBER() sur une CTE (ou table dérivée) partitionnée par la clé et filtrer WHERE rn = 1.

Avantages
  • Souvent très performantes et lisibles pour les classements par groupe
  • Contrôle précis avec ROW_NUMBER(), RANK(), FIRST_VALUE(), etc.
Inconvénients
  • Nécessite une étape supplémentaire (CTE/table dérivée) puis un filtrage
  • Moins naturel pour appeler une fonction table dépendante de chaque ligne (ex. OPENJSON/STRING_SPLIT)
CTE / tables dérivées

On peut encapsuler des sous‑requêtes corrélées ou des calculs intermédiaires dans une CTE et les rejoindre.

Avantages
  • Structure claire pour des transformations multi‑étapes
  • Facile à tester et à commenter par bloc
Inconvénients
  • Peut être plus verbeux que APPLY quand on a besoin d’une expansion par ligne
  • La lisibilité baisse si plusieurs CTE s’empilent
JOINs avec agrégations / sous‑requêtes

Pour récupérer une ligne par groupe, un JOIN sur une sous‑requête agrégée (ou une table dérivée munie d’un ROW_NUMBER()) est une alternative classique.

Avantages
  • Approche standard, facile à optimiser avec des index
  • Fonctionne sans fonctions table ni OPENJSON
Inconvénients
  • Moins flexible pour les expansions par ligne (unpivot) qu’avec APPLY
  • Manipuler les jeux de colonnes scalaires issus d’agrégations peut être moins direct

Résumé

Utilisez APPLY quand vous devez dériver des lignes/colonnes par ligne source via une expression tabulaire corrélée (fonction table inline, OPENJSON, STRING_SPLIT). Choisir CROSS vs OUTER selon le besoin de filtrer ou non les résultats vides.

Avantages
  • Expressif pour les expansions par ligne (dépivotage, parsing, top N par groupe)
  • Combinable avec ORDER BY/TOP pour des sélections scalaires par ligne
  • Alternative performante aux CTE complexes selon le cas
Inconvénients
  • Attention aux fonctions table multi‑instructions (moins optimisées que les fonctions table inline)
  • Peut dégrader les perfs si l’expression est lourde (évaluer le plan)
  • Fonctionnalités comme OPENJSON nécessitent SQL Server 2016+

Bonnes pratiques

  • Privilégiez les fonctions table inline (une seule SELECT) pour aider l’optimiseur
  • Filtrez le plus tôt possible (WHERE dans l’expression APPLY)
  • Ajoutez les index utiles sur les colonnes corrélées
  • Mesurez le plan d’exécution et comparez à des alternatives (JOIN, CTE)