Guide · Salesforce · SOQL

SOQL avancé : relations, semi-jointures et agrégats

Relations parent et enfant, semi-jointures, anti-jointures, GROUP BY, HAVING, littéraux de date et mode utilisateur : remplacer du code Apex par une seule requête.

Remonter vers le parent

SOQL ne connaît pas les JOIN du SQL : les relations se parcourent par la notation pointée. Depuis un enfant, on remonte jusqu’à cinq niveaux de parents :

SELECT Id, LastName, Account.Name, Account.Owner.Email
FROM Contact
WHERE Account.Industry = 'Insurance'

Pour une relation personnalisée, le suffixe devient __r : Agence__r.Region__c.

Descendre vers les enfants

Depuis un parent, une sous-requête dans le SELECT renvoie les enfants, en utilisant le nom de relation au pluriel (Contacts, Cases, ou Contrats__r pour un objet personnalisé) :

SELECT Id, Name,
       (SELECT Id, LastName, Email FROM Contacts WHERE Email != null ORDER BY LastName),
       (SELECT Id, Subject FROM Cases WHERE IsClosed = false)
FROM Account
WHERE Id IN :accountIds
for (Account acc : [SELECT Name, (SELECT LastName FROM Contacts) FROM Account WHERE Id IN :accountIds]) {
    System.debug(acc.Name + ' : ' + acc.Contacts.size() + ' contact(s)');
}

Une seule requête remplace une boucle de requêtes : c’est la première optimisation à chercher dans un trigger.

Semi-jointures et anti-jointures

Pour filtrer sur l’existence (ou l’absence) d’enregistrements liés, une sous-requête dans le WHERE :

Comptes ayant au moins une demande prioritaire ouverte (semi-jointure) :

SELECT Id, Name
FROM Account
WHERE Id IN (SELECT AccountId FROM Case WHERE IsClosed = false AND Priority = 'High')

Comptes sans aucune opportunité (anti-jointure) :

SELECT Id, Name
FROM Account
WHERE Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId != null)

La sous-requête ne renvoie qu’un champ, un identifiant ou une référence. Un WHERE accepte au plus deux sous-requêtes de ce type.

Agrégats : GROUP BY et HAVING

COUNT, SUM, AVG, MIN, MAX et COUNT_DISTINCT calculent côté serveur, sans rapatrier les lignes :

SELECT StageName, COUNT(Id) total, SUM(Amount) montant
FROM Opportunity
WHERE CloseDate = THIS_FISCAL_YEAR
GROUP BY StageName
HAVING COUNT(Id) > 10
ORDER BY SUM(Amount) DESC

En Apex, le résultat est une liste d’AggregateResult, lue par alias :

for (AggregateResult ar : [
    SELECT StageName, COUNT(Id) total, SUM(Amount) montant
    FROM Opportunity
    WHERE CloseDate = THIS_FISCAL_YEAR
    GROUP BY StageName
]) {
    String etape = (String) ar.get('StageName');
    Integer total = (Integer) ar.get('total');
    Decimal montant = (Decimal) ar.get('montant');
}

Attention : chaque ligne agrégée compte dans la limite de 50 000 lignes par transaction. Pour des agrégats sur des millions de lignes, préférer un rapport ou un traitement par lot.

Littéraux de date

Les littéraux évitent de calculer des bornes en Apex et suivent le fuseau horaire de l’utilisateur :

SELECT Id FROM Case WHERE CreatedDate = LAST_N_DAYS:30
SELECT Id FROM Opportunity WHERE CloseDate = NEXT_QUARTER
SELECT Id FROM Task WHERE ActivityDate < TODAY AND IsClosed = false
SELECT CALENDAR_MONTH(CreatedDate) mois, COUNT(Id) FROM Case
WHERE CreatedDate = THIS_YEAR GROUP BY CALENDAR_MONTH(CreatedDate)

Les plus utiles : TODAY, YESTERDAY, LAST_N_DAYS:n, NEXT_N_DAYS:n, THIS_MONTH, LAST_QUARTER, THIS_FISCAL_YEAR.

TYPEOF, toLabel et FORMAT

Relation polymorphe, avec des champs différents selon le type de l’objet lié :

SELECT Id, Subject,
       TYPEOF What
           WHEN Account THEN Name, Industry
           WHEN Opportunity THEN Name, StageName
       END
FROM Event

Libellé traduit d’une liste de sélection et montant formaté selon l’utilisateur :

SELECT Id, toLabel(StageName), FORMAT(Amount) FROM Opportunity

TYPEOF évite de lancer une requête par type d’objet lié ; toLabel() renvoie le libellé dans la langue de l’utilisateur au lieu de la valeur d’API ; FORMAT() met en forme nombres, devises et dates.

Sécurité et requêtes dynamiques

// Mode utilisateur : partage, droits d'objet et de champ appliqués ; exception si un champ est interdit.
List<Account> comptes = [SELECT Id, Name, AnnualRevenue FROM Account WITH USER_MODE];

// Requête dynamique sans concaténation : les variables passent par une Map de liaison.
String soql = 'SELECT Id, Name FROM Account WHERE Industry = :secteur AND CreatedDate > :depuis';
List<Account> resultats = Database.queryWithBinds(
    soql,
    new Map<String, Object>{ 'secteur' => secteurChoisi, 'depuis' => Datetime.now().addDays(-90) },
    AccessLevel.USER_MODE
);
  • WITH USER_MODE remplace avantageusement WITH SECURITY_ENFORCED : il couvre aussi le partage et la clause WHERE.
  • Une requête dynamique ne concatène jamais une saisie : Database.queryWithBinds élimine l’injection SOQL.

Sur de gros volumes, ces requêtes doivent aussi être sélectives : voir SOQL : écrire des requêtes sélectives.

🍪 Gestion des cookies
Ce site utilise des cookies pour mesurer l'audience (Google Analytics) et lire des vidéos (YouTube). Vous pouvez personnaliser votre choix.
Mentions légales
Réglages des cookies
Essentiels Nécessaires au fonctionnement du site — ne peuvent pas être désactivés.
Google Analytics Analyse d'audience (pages vues, sources de trafic). Aucune donnée personnelle identifiante.
YouTube Permet de lire les vidéos YouTube intégrées sur cette page.