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_MODEremplace avantageusementWITH SECURITY_ENFORCED: il couvre aussi le partage et la clauseWHERE.- 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.