← Retour aux Insights

Dates et fuseaux dans Marketing Cloud : pourquoi vos emails d'anniversaire partent un jour trop tard

Pierre Frin Septembre 2026 7 min de lecture
automation planifiée à 7 h, heure de Paris hiver · 7 h à Paris serveur : 0 h, jour J été · 7 h à Paris serveur : 23 h, jour J-1 CAST(GETDATE() AS DATE) comparé au jour de naissance anniversaires de la veille Marketing Cloud Engagement · heure système UTC-6, sans heure d'été

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.

Le serveur ne vit pas à Paris

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.

SourceFuseauRemarque
GETDATE()Heure système, UTC-6 toute l'annéeDécalage constant avec UTC
GETUTCDATE()UTCLe même instant, exprimé en UTC
EventDate des data viewsHeure système, UTC-6Arrondie à la seconde pour les ouvertures et les clics
Champ Date d'une data extensionAucun fuseau attachéLa valeur est celle qui a été écrite, seule la convention de l'équipe dit ce qu'elle représente

Sept ou huit heures d'écart

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.

Le calcul qui explique l'incident

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.

Le symptôme d'un problème de fuseau

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.

Calculer la date de référence une seule fois

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 table de bascules pour le décalage

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.

Des fenêtres qui ne dépendent pas de l'heure d'exécution

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.

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.

L'anniversaire, le 29 février et l'âge

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)

Corriger l'âge calculé par DATEDIFF

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.

Le cas du 29 février

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.

Si vous venez d'Adobe Campaign

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.

Le réflexe à garder

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.

Questions fréquentes

Quelle est l'heure système de Marketing Cloud ?

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.

Pourquoi l'email d'anniversaire part-il un jour trop tard seulement en été ?

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.

Peut-on ajouter un décalage fixe de sept heures pour obtenir l'heure de Paris ?

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.

Comment écrire une fenêtre « hier à Paris » sur une data view ?

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.

Pourquoi faut-il corriger DATEDIFF(YEAR, ...) pour calculer un âge ?

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.

Dans la même série
Pierre Frin
Fondateur Grokium · Consultant CRM · Certifié Adobe Campaign Classic et Salesforce Marketing Cloud Email Specialist

Vos requêtes savent-elles quel jour on est ?

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 →