Un site de vente d’articles de sport prépare chaque matin la cible de sa newsletter promotionnelle. Une automation lancée à 6 h 00 exécute une seule requête, écrite au lancement du programme deux ans plus tôt : clients français ou belges, ayant commandé dans les 90 derniers jours, sans bounce récent. À l’époque, elle tournait en quatre minutes.
Depuis, la table des commandes a été multipliée par six, et la même requête a été recopiée dans trois autres automations planifiées elles aussi à 6 h 00. Un lundi, l’activité s’arrête au bout de 30 minutes. L’envoi, planifié séparément à 8 h 00, part quand même : il lit la data extension cible, qui contient encore la population du vendredi.
En recette, sur une copie réduite des données, la requête passe en une minute. Rien dans le code n’est faux. C’est sa forme qui ne tient plus le volume.
SELECT DISTINCT
c.ClientId,
c.Email,
c.Prenom
FROM Clients AS c
INNER JOIN Commandes AS co
ON co.ClientId = c.ClientId
WHERE (c.Pays = 'FR' OR c.Pays = 'BE')
AND DATEDIFF(day, co.DateCommande, GETDATE()) <= 90
AND UPPER(c.Email) NOT LIKE '%@EXAMPLE.ORG'
AND NOT EXISTS (
SELECT 1
FROM _Bounce AS b
WHERE b.SubscriberKey = c.ClientId
AND DATEDIFF(day, b.EventDate, GETDATE()) <= 30
)
Une requête de Marketing Cloud Engagement n’a pas de plan d’exécution consultable et pas d’index que vous pouvez créer vous-même. Elle a en revanche une durée maximale de 30 minutes, au-delà de laquelle l’activité est interrompue. Tant que les volumes restent modestes, rien de tout cela ne se voit.
| Limite | Valeur documentée | Nature |
|---|---|---|
| Durée d’exécution d’une requête | 30 minutes | Stricte |
| Durée visée pour de bonnes performances | Moins de 5 minutes | Recommandation |
| Requête qui dépasse régulièrement 10 minutes | Envisager un autre outil de transformation | Recommandation |
| Data extensions par requête | 5, quatre ou moins recommandé | Souple |
| Jointures par requête | 4, trois ou moins recommandé | Souple |
| Écrasements en mode Overwrite par automation | 5 | Souple |
La requête de l’incident ne dépasse aucun de ces seuils sur le papier : deux data extensions, une data view, une jointure. Elle échoue quand même, parce que le volume traité et la forme des conditions pèsent davantage que le nombre de tables. La cible de cinq minutes reste le meilleur repère, puisqu’une requête qui tient quinze minutes en semaine normale dépassera 30 minutes le jour où la plateforme est chargée.
⚠️ Le signal à surveiller : l’historique d’Automation Studio conserve la durée de chaque exécution. Une progression régulière sur quelques semaines annonce le délai dépassé bien avant qu’il se produise. Personne ne regarde ces durées tant que rien n’a cassé, et c’est précisément là qu’elles servent.
Le dialecte est basé sur SQL Server sans lui correspondre exactement. Les variables, les curseurs, les tables temporaires et les expressions de table communes avec WITH ne sont pas pris en charge, pas plus que les commentaires de fin de ligne à deux tirets. Les découpages décrits plus bas passent donc par de vraies data extensions, et les commentaires s’écrivent entre /* et */.
Vous ne créez pas d’index dans Marketing Cloud. Salesforce indique que la plateforme indexe automatiquement les clés primaires, les champs de relation d’envoi et les champs les plus utilisés, et signale quelques colonnes indexées sur les data views. Votre levier consiste à écrire des conditions que le moteur peut résoudre avec ces index.
La page d’optimisation de Salesforce demande d’éviter dans un WHERE les arguments qualifiés de non indexables : OR, NOT, NOT EXISTS, NOT IN, NOT LIKE, ainsi que les fonctions appliquées à la valeur d’une colonne. La même page réécrit pourtant un DATEDIFF en intervalle borné. Ce qui coûte, c’est la fonction qui enveloppe la colonne, pas la comparaison sur une colonne nue.
| À éviter | Réécriture | Pourquoi |
|---|---|---|
DATEDIFF(day, DateCommande, GETDATE()) <= 90 | DateCommande >= DATEADD(day, -90, CAST(GETDATE() AS DATE)) | Le calcul porte sur la constante, la colonne reste nue |
UPPER(Email) = ... | Email = ... | La comparaison de texte ignore la casse avec la collation habituelle |
Pays = 'FR' OR Pays = 'BE' | Jointure sur une table Pays_Cibles, ou deux requêtes réunies par UNION ALL | Chaque branche redevient une égalité simple |
NOT EXISTS sur une grosse table | Table d’exclusion préparée à part, puis LEFT JOIN ... IS NULL | La partie coûteuse est calculée une fois, sur un volume réduit |
LIKE '%texte' | Colonne calculée à l’alimentation, le domaine de l’adresse par exemple | Un joker en tête de motif interdit l’usage d’un index |
Le LEFT JOIN ... IS NULL n’est pas cité en toutes lettres par Salesforce. Il s’appuie sur une jointure d’égalité avec une petite table dont la clé primaire est indexée, ce qui le rend plausible, mais mesurez-le sur vos données avant d’en faire une règle.
La table de paramétrage a un avantage discret : Pays_Cibles contient deux lignes, FR et BE, et ajouter la Suisse ne demandera plus de rouvrir la requête ni de la revalider.
Avant de réécrire quoi que ce soit, découpez la requête et exécutez chaque morceau seul, dans une activité de test qui écrit dans une data extension jetable. Comparez le nombre de lignes de Commandes sur 90 jours au nombre de clients distincts : si le rapport est de dix pour un, la requête d’origine fabriquait dix lignes par client avant que le DISTINCT n’en supprime neuf. Le morceau le plus long désigne la priorité.
Salesforce recommande ensuite de casser les requêtes à jointures multiples en requêtes plus petites qui écrivent dans des tables intermédiaires, puis de consolider avec une dernière requête. Chaque sous-ensemble coûteux se calcule une fois, en mode Overwrite, et ne garde qu’une ligne par client.
/* Tmp_Acheteurs_90j, clé primaire ClientId, mode Overwrite */
SELECT
co.ClientId,
MAX(co.DateCommande) AS DerniereCommande
FROM Commandes AS co
WHERE co.DateCommande >= DATEADD(day, -90, CAST(GETDATE() AS DATE))
GROUP BY co.ClientId
Le GROUP BY supprime le besoin de DISTINCT dans la requête finale. Salesforce recommande de réserver SELECT DISTINCT aux cas où les doublons existent réellement dans les données, plutôt que de s’en servir pour rattraper une jointure trop large. La table des bounces récents se prépare de la même façon, avec SubscriberKey pour clé primaire.
La requête finale ne manipule plus que des tables réduites, reliées par leurs clés.
/* Cible_Newsletter, clé primaire ClientId, mode Overwrite */
SELECT
c.ClientId,
c.Email,
c.Prenom
FROM Clients AS c
INNER JOIN Pays_Cibles AS p
ON p.Pays = c.Pays
INNER JOIN Tmp_Acheteurs_90j AS a
ON a.ClientId = c.ClientId
LEFT JOIN Tmp_Bounces_30j AS b
ON b.SubscriberKey = c.ClientId
WHERE c.Email IS NOT NULL
AND b.SubscriberKey IS NULL
Trois jointures, toutes sur des égalités de clés, dans la recommandation de Salesforce. Le filtre sur le domaine de l’adresse a disparu : un joker en tête de motif ne s’accélère pas, et s’il vous est vraiment utile, il devient une colonne calculée à l’import. Les dates sont exprimées dans l’heure du serveur, comme EventDate et GETDATE() ; si vos dates de commande sont en heure de Paris, les bornes décalent de quelques heures.
Les trois requêtes deviennent trois étapes successives de la même automation, dans l’ordre des dépendances, puis vient l’envoi. Salesforce recommande de ne mettre qu’une requête par étape et de décaler les automations entre elles, sur des heures moins demandées : 5 h 40 plutôt que 6 h 00.
Tmp_Acheteurs_90j en Overwrite, la plus lourde, mesurée à part ;Tmp_Bounces_30j en Overwrite, rapide et de faible volume ;Cible_Newsletter en Overwrite, jointures sur clés uniquement ;Ramener l’envoi dans l’automation supprime la cause directe de l’incident : il n’est plus déclenché par une horloge indépendante du calcul qui l’alimente. Quatre écrasements restent sous le seuil de cinq par automation. Quant aux trois automations qui recopiaient la requête, le mieux est de les faire lire Cible_Newsletter au lieu de refaire le même calcul trois fois.
💡 Écriture en mode Update trop longue : un article de support explique que le temps part surtout dans la vérification de l’existence de chaque ligne. Il recommande d’écrire le résultat dans une data extension intermédiaire, puis de la charger avec une activité d’import en ajout et mise à jour, qui n’est pas soumise au délai de 30 minutes. Salesforce conseille aussi de préférer Overwrite à Update quand c’est possible.
Dans Adobe Campaign Classic, une activité de requête génère un SQL que vous pouvez lire dans les journaux, et vous déclarez vos index dans le schéma. Rien de tel ici : pas de plan d’exécution, pas d’index à la demande, et la seule mesure disponible est la durée d’exécution relevée après coup.
Les tables de travail qu’un workflow Campaign crée et purge tout seul entre deux activités n’existent pas non plus. Leur équivalent est une data extension intermédiaire que vous créez, nommez et écrasez vous-même à chaque exécution. C’est plus de plomberie, mais c’est aussi la seule façon de garder la main sur ce que le moteur traite à chaque étape.
Relevez les durées d’exécution avant de toucher au SQL. Si une requête tourne en douze minutes aujourd’hui, elle n’a pas besoin d’être optimisée : elle a besoin d’être découpée, parce que le volume qui la fera tomber est déjà en train d’arriver.
La duree d'execution d'une requete est limitee a 30 minutes, et cette limite est stricte : au-dela, l'activite est interrompue. La duree visee pour de bonnes performances est inferieure a 5 minutes, et une requete qui depasse regulierement 10 minutes doit faire envisager un autre outil de transformation.
Non, une requete de Marketing Cloud Engagement n'a pas de plan d'execution consultable et pas d'index que vous pouvez creer vous-meme. Salesforce indique que la plateforme indexe automatiquement les cles primaires, les champs de relation d'envoi et les champs les plus utilises, et signale quelques colonnes indexees sur les data views. Votre levier consiste a ecrire des conditions que le moteur peut resoudre avec ces index.
On remplace DATEDIFF(day, DateCommande, GETDATE()) <= 90 par DateCommande >= DATEADD(day, -90, CAST(GETDATE() AS DATE)). Le calcul porte alors sur la constante et la colonne reste nue. Ce qui coute, c'est la fonction qui enveloppe la colonne, pas la comparaison sur une colonne nue.
Un article de support explique que le temps part surtout dans la verification de l'existence de chaque ligne. Il recommande d'ecrire le resultat dans une data extension intermediaire, puis de la charger avec une activite d'import en ajout et mise a jour, qui n'est pas soumise au delai de 30 minutes. Salesforce conseille aussi de preferer Overwrite a Update quand c'est possible.
Non. Le dialecte est base sur SQL Server sans lui correspondre exactement : les variables, les curseurs, les tables temporaires et les expressions de table communes avec WITH ne sont pas pris en charge, pas plus que les commentaires de fin de ligne a deux tirets. Les decoupages passent donc par de vraies data extensions, et les commentaires s'ecrivent entre /* et */.
Je peux relire vos requêtes, mesurer leurs durées et vous rendre un diagnostic écrit avec le découpage à mettre en place chez vous.
Me décrire votre contexte →