Une enseigne de cosmétiques envoie chaque matin un email d'anniversaire avec une offre valable une semaine. Une automation planifiée à 7 h, heure de Paris, sélectionne les clients nés ce jour-là et alimente un parcours. La requête compare le jour et le mois de naissance à la date du serveur.
Pendant tout l'hiver, rien à signaler. Début avril, plusieurs clientes écrivent au service client : elles ont reçu leur email le lendemain de leur anniversaire. L'équipe relit la requête, la relance à la main en milieu de matinée, et le résultat est correct. Le ticket est clos par erreur, puis rouvert la semaine suivante.
La cause n'est pas dans la requête. Elle est dans l'écart entre l'heure à laquelle l'automation se déclenche et l'heure que le moteur SQL croit être.
La documentation Salesforce est explicite sur ce point : l'heure système de Marketing Cloud est l'heure standard du centre de l'Amérique du Nord, UTC-6, sans passage à l'heure d'été. Les pages des data views le répètent pour leurs propres dates. Le décalage avec Paris, lui, change deux fois par an.
| Source | Fuseau | Remarque |
|---|---|---|
GETDATE() | Heure système, UTC-6 toute l'année | Décalage constant avec UTC |
GETUTCDATE() | UTC | Le même instant, exprimé en UTC |
EventDate des data views | Heure système, UTC-6 | Arrondie à la seconde pour les ouvertures et les clics |
| Champ Date d'une data extension | Aucun fuseau attaché | La valeur est celle qui a été écrite, seule la convention de l'équipe dit ce qu'elle représente |
Paris est à UTC+1 en hiver et UTC+2 en été, le serveur à UTC-6 toute l'année. L'écart vaut donc sept heures d'octobre à mars, huit heures le reste de l'année, avec une bascule le dernier dimanche de mars et le dernier dimanche d'octobre. Ce sont ces deux dimanches qui font apparaître et disparaître le bug, ce qui explique les diagnostics qui traînent.
En hiver, 7 h à Paris correspond à 6 h UTC, soit minuit pile à l'heure du serveur. La date système est bien celle du jour, et la requête tombe juste. En été, 7 h à Paris correspond à 5 h UTC, soit 23 h la veille à l'heure du serveur. Toute la saison, la requête a sélectionné les anniversaires de la veille.
Relancée à 10 h par l'équipe, elle redevenait correcte, puisque le serveur avait changé de jour entre-temps. Une requête qui donne le bon résultat quand on la teste à la main et le mauvais quand elle s'exécute seule, c'est le symptôme typique d'un problème de fuseau.
⚠️ Un décalage fixe pour Paris est faux la moitié de l'année. Écrire DATEADD(HOUR, 7, o.EventDate) donne l'heure de Paris de fin octobre à fin mars, et une heure de moins que la réalité le reste du temps. Le décalage fixe n'est valable que pour passer de l'heure système à UTC, puisque ni l'une ni l'autre ne change d'heure.
La Query Activity n'accepte ni variables ni expressions de table communes. Recopier une conversion dans chaque requête garantit qu'une des copies finira par diverger. La convention qui tient dans la durée consiste à calculer les dates de référence en tête d'automation, dans une data extension de paramètres à une seule ligne, puis à joindre cette ligne partout ailleurs.
Une petite table de bascules, maintenue à la main pour quelques années, suffit à obtenir le décalage du moment. Chaque ligne couvre une période et porte le décalage de Paris par rapport à UTC, 1 ou 2.
/* Cible : Parametres_Date, mode Overwrite, Cle en clé primaire */
SELECT
'J' AS Cle,
CAST(DATEADD(HOUR, b.DecalageParis, GETUTCDATE()) AS DATE) AS AujourdhuiParis,
DATEADD(HOUR, -(b.DecalageParis + 6),
CAST(DATEADD(DAY, -1,
CAST(DATEADD(HOUR, b.DecalageParis, GETUTCDATE()) AS DATE)
) AS DATETIME)) AS DebutVeilleSys,
DATEADD(HOUR, -(b.DecalageParis + 6),
CAST(CAST(DATEADD(HOUR, b.DecalageParis, GETUTCDATE()) AS DATE)
AS DATETIME)) AS FinVeilleSys
FROM Bascules_Heure AS b
WHERE GETUTCDATE() >= b.DebutUTC
AND GETUTCDATE() < b.FinUTC
AujourdhuiParis donne la date du jour à Paris quelle que soit l'heure d'exécution. Les deux autres colonnes donnent minuit de la veille et minuit du jour, exprimés en heure système, prêts à être comparés à une EventDate. Sur le jour d'une bascule, contrôlez le résultat à la main la première année : les bornes de la veille relèvent du décalage précédent. Une Verification Activity placée juste après vérifie que la table contient bien une ligne, puisque toutes les requêtes suivantes en dépendent.
💡 La piste AT TIME ZONE reste à vérifier chez vous. SQL Server 2016 propose cette syntaxe pour déclarer le fuseau d'une date puis la convertir avec les règles d'heure d'été du fuseau cible. La documentation Salesforce ne la cite pas dans sa liste de fonctions, même si des retours de la communauté en montrent l'usage dans des Query Activities. Si elle passe la validation et l'exécution sur votre compte, elle remplace avantageusement la table de bascules. Testez-la avant d'en dépendre, et gardez le repli en tête.
Avec la ligne de paramètres, une fenêtre « hier à Paris » sur une data view se réduit à une jointure et à deux comparaisons de dates système.
SELECT
o.SubscriberKey,
o.JobID,
MIN(o.EventDate) AS PremiereOuverture
FROM _Open AS o
INNER JOIN Parametres_Date AS p
ON p.Cle = 'J'
WHERE o.EventDate >= p.DebutVeilleSys
AND o.EventDate < p.FinVeilleSys
GROUP BY o.SubscriberKey, o.JobID
Trois choix méritent d'être explicités dans cette requête.
EventDate dans la clause WHERE. Salesforce recommande de l'éviter, parce qu'une écriture du type CAST(o.EventDate AS DATE) = ... empêche l'usage des index sur une data view volumineuse.GROUP BY garantit une ligne par couple de clés, même si un abonné a ouvert plusieurs fois le même envoi.Sur l'interprétation de ces ouvertures, gonflées chez une partie des abonnés par le chargement automatique des images, l'article sur les data views donne le détail.
La requête d'audience joint la ligne de paramètres et compare le jour et le mois de naissance à la date de Paris, pas à celle du serveur.
SELECT
c.ClientId,
c.Email,
c.Prenom,
DATEDIFF(YEAR, c.DateNaissance, p.AujourdhuiParis)
- CASE WHEN MONTH(c.DateNaissance) > MONTH(p.AujourdhuiParis)
OR (MONTH(c.DateNaissance) = MONTH(p.AujourdhuiParis)
AND DAY(c.DateNaissance) > DAY(p.AujourdhuiParis))
THEN 1 ELSE 0 END AS Age
FROM Clients AS c
INNER JOIN Parametres_Date AS p
ON p.Cle = 'J'
WHERE c.Email IS NOT NULL
AND MONTH(c.DateNaissance) = MONTH(p.AujourdhuiParis)
AND DAY(c.DateNaissance) = DAY(p.AujourdhuiParis)
Le calcul de l'âge corrige DATEDIFF(YEAR, ...), qui compte les passages au 1er janvier et non les années révolues. Une personne née le 20 décembre 2000 aurait 26 ans dès le 1er janvier 2026 alors qu'elle en a 25. Le jour de l'anniversaire, la correction vaut zéro, mais la même expression resservira sur une offre réservée aux majeurs, où l'erreur n'est plus cosmétique.
Restent les personnes nées un 29 février, que cette requête ignore trois années sur quatre. C'est une règle métier avant d'être une règle SQL : certaines marques les traitent le 28, d'autres le 1er mars. Tranchez avec le marketing, écrivez la règle dans la requête, et documentez-la. Dernier détail de performance : MONTH et DAY appliquées à DateNaissance empêchent l'usage d'un index. Sur plusieurs millions de clients, stockez le mois et le jour de naissance dans deux champs numériques calculés à l'import.
Adobe Campaign distingue les dates stockées avec fuseau, ramenées en UTC en base, et les dates sans fuseau, selon la configuration de l'instance, puis affiche le tout dans le fuseau de l'opérateur. Marketing Cloud n'offre pas cette mécanique en SQL. Les dates des data views sont en UTC-6 fixe, les champs Date d'une data extension n'ont aucun fuseau attaché, et la conversion est à écrire dans chaque requête. D'où l'intérêt de la calculer une seule fois.
Le planificateur d'un workflow et la planification d'une automation se ressemblent beaucoup, tous deux déclenchent à une heure locale. Le piège vient du fait que le SQL, lui, raisonne dans un autre fuseau que la planification. C'est exactement ce qui a produit l'incident de l'enseigne.
Pour chaque date que manipule une requête, sachez dire dans quel fuseau elle est. Si vous ne savez pas répondre pour un champ de data extension, la convention n'est écrite nulle part, et c'est le moment de la fixer. Le reste suit : une conversion, une seule fois, en tête d'automation.
L'heure système de Marketing Cloud est l'heure standard du centre de l'Amérique du Nord, UTC-6, sans passage à l'heure d'été. GETDATE() renvoie cette heure, GETUTCDATE() renvoie le même instant exprimé en UTC. L'écart avec Paris vaut sept heures d'octobre à mars et huit heures le reste de l'année.
En hiver, 7 h à Paris correspond à 6 h UTC, soit minuit pile à l'heure du serveur : la date système est celle du jour et la requête tombe juste. En été, 7 h à Paris correspond à 5 h UTC, soit 23 h la veille à l'heure du serveur, et la requête sélectionne les anniversaires de la veille. C'est la bascule du dernier dimanche de mars qui fait apparaître le problème.
Non, un décalage fixe pour Paris est faux la moitié de l'année. Écrire DATEADD(HOUR, 7, o.EventDate) donne l'heure de Paris de fin octobre à fin mars, et une heure de moins que la réalité le reste du temps. Le décalage fixe n'est valable que pour passer de l'heure système à UTC, puisque ni l'une ni l'autre ne change d'heure.
La Query Activity n'accepte ni variables ni expressions de table communes, donc les dates de référence se calculent une seule fois en tête d'automation, dans une data extension de paramètres à une seule ligne, que l'on joint ensuite partout ailleurs. La fenêtre se réduit alors à une jointure et à deux comparaisons de dates système, avec un intervalle semi-ouvert : borne basse incluse, borne haute exclue. Aucune fonction n'est appliquée à EventDate dans la clause WHERE, car une écriture du type CAST(o.EventDate AS DATE) = ... empêche l'usage des index.
DATEDIFF(YEAR, ...) compte les passages au 1er janvier et non les années révolues. Une personne née le 20 décembre 2000 aurait 26 ans dès le 1er janvier 2026 alors qu'elle en a 25. Le jour de l'anniversaire, la correction vaut zéro, mais la même expression resservira sur une offre réservée aux majeurs, où l'erreur n'est plus cosmétique.
Je peux relire vos requêtes datées, vos fenêtres quotidiennes et vos automations planifiées, et vous rendre un diagnostic écrit avec les corrections à appliquer.
Me décrire votre contexte →