Guide · Salesforce · SOQL
SOQL : écrire des requêtes sélectives sur de gros volumes
Champs indexés, seuils de sélectivité, opérateurs qui empêchent l’index et Query Plan : éviter l’erreur « non-selective query » sur un objet de plusieurs millions de lignes.
L’erreur « non-selective query »
Sur un objet de quelques milliers de lignes, toute requête SOQL est rapide. Au-delà de quelques centaines de milliers, Salesforce exige qu’une requête lancée depuis un trigger soit sélective : qu’elle puisse s’appuyer sur un index au lieu de parcourir toute la table. Sinon :
// Trigger sur Contrat__c (5 millions de lignes) : Statut__c n'est pas indexé.
List<Contrat__c> actifs = [
SELECT Id, Numero__c
FROM Contrat__c
WHERE Statut__c = 'Actif' AND Agence__c != null
];
// System.QueryException: Non-selective query against large object type (more than 200000 rows) Hors trigger, la requête passe, mais elle reste lente et peut dépasser les limites de temps. Exemple fil rouge : un objet fictif Contrat__c de cinq millions de lignes, rattaché à une Agence__c.
Les champs indexés
Sont indexés d’office :
Id,Name,OwnerId,RecordTypeId;- les dates système
CreatedDateetSystemModstamp; - les champs de relation (lookup et master-detail) ;
- les champs marqués External ID ou Unique.
Pour un autre champ souvent filtré, deux options : le déclarer External ID quand c’est un identifiant métier, ou demander un index personnalisé au support Salesforce.
Les seuils de sélectivité
Un index n’est utilisé que si le filtre renvoie une part assez faible de la table :
| Index | Seuil | Plafond |
|---|---|---|
| Standard | 30 % du premier million de lignes, puis 15 % au-delà | 1 000 000 de lignes |
| Personnalisé | 10 % du premier million de lignes, puis 5 % au-delà | 333 333 lignes |
Sur nos cinq millions de contrats, un filtre sur un champ à index personnalisé doit donc renvoyer moins de 300 000 lignes (10 % de 1 million plus 5 % de 4 millions). Avec plusieurs conditions en AND, il suffit qu’une seule soit sélective ; avec OR, il faut que chacune le soit.
// Filtre d'abord sur des champs indexés (lookup + date système), puis affine.
List<Contrat__c> aRelancer = [
SELECT Id, Numero__c, Statut__c
FROM Contrat__c
WHERE Agence__c = :agenceId // lookup : indexé
AND CreatedDate = LAST_N_DAYS:30 // champ système : indexé
AND Statut__c = 'Actif' // filtre complémentaire
LIMIT 2000
];Ce qui empêche l’index
- Les négations :
!=,NOT IN,NOT LIKE,EXCLUDES. - Un joker en début de motif :
LIKE '%dupont';LIKE 'dupont%'reste indexable. - La comparaison à
nullsur un champ indexé, dans la plupart des cas. - Les formules non déterministes (qui dépendent de la date du jour ou d’un objet parent) : elles ne peuvent pas être indexées.
- Les données asymétriques : un statut porté par 90 % des lignes n’est jamais sélectif, même indexé.
Lire le Query Plan
Le Query Plan de la Developer Console, ou le paramètre explain de l’API REST, montre les plans envisagés par l’optimiseur et leur coût relatif :
GET /services/data/v62.0/query?explain=SELECT+Id+FROM+Contrat__c+WHERE+Agence__c='a01...'
{
"plans": [
{
"leadingOperationType": "Index",
"relativeCost": 0.04,
"cardinality": 1250,
"fields": ["Agence__c"],
"sobjectCardinality": 5000000,
"sobjectType": "Contrat__c"
},
{
"leadingOperationType": "TableScan",
"relativeCost": 2.8,
"cardinality": 1250,
"fields": [],
"sobjectCardinality": 5000000,
"sobjectType": "Contrat__c"
}
]
} Le plan le moins coûteux est en tête. Un relativeCost inférieur à 1 signale une requête sélective ; un TableScan en tête signifie que la table entière sera parcourue.
Côté Apex
La sélectivité ne sert à rien si la requête est lancée mille fois. Dans un trigger ou un batch, on regroupe les identifiants et on interroge une seule fois :
// À éviter : une requête par enregistrement (limite de 100 requêtes en synchrone).
for (Contrat__c c : Trigger.new) {
Agence__c a = [SELECT Region__c FROM Agence__c WHERE Id = :c.Agence__c];
}
// Une requête pour tout le lot.
Set<Id> agenceIds = new Set<Id>();
for (Contrat__c c : Trigger.new) {
agenceIds.add(c.Agence__c);
}
Map<Id, Agence__c> agences = new Map<Id, Agence__c>(
[SELECT Id, Region__c FROM Agence__c WHERE Id IN :agenceIds]
); Rappel des limites par transaction : 100 requêtes SOQL en synchrone, 200 en asynchrone, et 50 000 lignes renvoyées au total. Pour parcourir plus de lignes, utiliser un Batch Apex ou la Bulk API 2.0 côté intégration.