Aller au contenu principal

Dates et Timestamp

important

Cette documentation a été retirée et pourrait ne pas être mise à jour. Les produits, services ou technologies mentionnés dans ce contenu ne sont plus pris en charge. Consultez les modèles de date-heure.

Les types de données Date et Timestamp ont changé de manière significative dans Databricks Runtime 7.0. Cet article décrit :

  • Le type Date et le calendrier associé.
  • Le type Timestamp et son lien avec les fuseaux horaires. Il explique également les détails de la résolution du décalage de fuseau horaire et les changements subtils de comportement dans la nouvelle API horaire de Java 8, utilisée par Databricks Runtime 7.0.
  • APIs pour construire des valeurs de date et de timestamp.
  • Pièges courants et bonnes pratiques pour la collecte d'objets de date et de Timestamp sur le Driver Apache Spark.

Dates et calendriers

Un Date est une combinaison des champs année, mois et jour, comme (année=2012, mois=12, jour=31). Cependant, les valeurs des champs année, mois et jour ont des contraintes pour garantir que la valeur de la date est une date valide dans le monde réel. Par exemple, la valeur du mois doit être de 1 à 12, la valeur du jour doit être de 1 à 28, 29, 30 ou 31 (selon l'année et le mois), et ainsi de suite. Le type Date ne tient pas compte des fuseaux horaires.

Calendriers

Les contraintes sur les champs Date sont définies par l'un des nombreux calendriers possibles. Certains, comme le calendrier lunaire, ne sont utilisés que dans des régions spécifiques. Certains, comme le calendrier julien, ne sont utilisés que dans l'histoire. La norme internationale de facto est le calendrier grégorien qui est utilisé presque partout dans le monde à des fins civiles. Il a été introduit en 1582 et a été étendu pour prendre en charge également les dates antérieures à 1582. Ce calendrier étendu est appelé le calendrier grégorien proleptique.

Databricks Runtime 7.0 utilise le calendrier grégorien proleptique, qui est déjà utilisé par d'autres systèmes de données comme Pandas, R et Apache Arrow. Databricks Runtime 6.x et versions antérieures utilisaient une combinaison du calendrier julien et grégorien : pour les dates antérieures à 1582, le calendrier julien était utilisé ; pour les dates postérieures à 1582, le calendrier grégorien était utilisé. Ceci est hérité de l'API java.sql.Date héritée, qui a été remplacée en Java 8 par java.time.LocalDate, qui utilise le calendrier grégorien proleptique.

Timestamp et fuseaux horaires

Le type Timestamp étend le type Date avec de nouveaux champs : heure, minute, seconde (qui peut avoir une partie fractionnaire) et avec un fuseau horaire global (limité à la session). Il définit un instant temporel concret. Par exemple, (année=2012, mois=12, jour=31, heure=23, minute=59, seconde=59,123456) avec le fuseau horaire de session UTC+01:00. Lors de l’écriture de valeurs de timestamp dans des sources de données non textuelles comme Parquet, les valeurs sont de simples instants (comme le timestamp en UTC) qui n’ont aucune information de fuseau horaire. Si vous écrivez et lisez une valeur de timestamp avec un fuseau horaire de session différent, vous pouvez voir des valeurs différentes des champs heure, minute et seconde, mais elles correspondent au même instant temporel concret.

Les champs des heures, des minutes et des secondes ont des plages standard : de 0 à 23 pour les heures et de 0 à 59 pour les minutes et les secondes. Spark prend en charge les secondes fractionnées avec une précision à la microseconde. La plage de valeurs valides pour les fractions est de 0 à 999 999 microsecondes.

À tout instant donné, selon le fuseau horaire, vous pouvez observer de nombreuses valeurs d'horloge murale différentes :

Horloges murales

Inversement, une valeur d'horloge murale peut représenter de nombreux instants temporels différents.

Le décalage de fuseau horaire vous permet de lier de manière non ambiguë un timestamp local à un instant t. Généralement, les décalages de fuseau horaire sont définis comme des décalages horaires par rapport à l'heure moyenne de Greenwich (GMT) ou UTC+0 (temps universel coordonné). Cette représentation des informations de fuseau horaire élimine l'ambiguïté, mais elle est peu pratique. La plupart des gens préfèrent indiquer un emplacement tel que America/Los_Angeles ou Europe/Paris. Ce niveau d'abstraction supplémentaire par rapport aux décalages de fuseau horaire simplifie la vie, mais apporte des complications. Par exemple, vous devez désormais maintenir une base de données spéciale de fuseaux horaires pour faire correspondre les noms de fuseaux horaires aux décalages. Étant donné que Spark s'exécute sur la JVM, il délègue le mappage à la bibliothèque standard Java, qui charge les données depuis la base de données des fuseaux horaires de l'Internet Assigned Numbers Authority (IANA TZDB). En outre, le mécanisme de mappage de la bibliothèque standard de Java présente des nuances qui influencent le comportement de Spark.

Depuis Java 8, le JDK a exposé une API différente pour la manipulation des dates et heures et la résolution des décalages horaires et Databricks Runtime 7.0 utilise cette API. Bien que le mappage des noms de fuseaux horaires aux décalages ait la même source, IANA TZDB, il est implémenté différemment dans Java 8 et versions ultérieures par rapport à Java 7.

Par exemple, examinez un Timestamp antérieur à l'année 1883 dans le fuseau horaire America/Los_Angeles : 1883-11-10 00:00:00. Cette année se distingue des autres car le 18 novembre 1883, tous les chemins de fer nord-américains sont passés à un nouveau système d'heure standard. En utilisant l'API de temps Java 7, vous pouvez obtenir un décalage de fuseau horaire à l'horodatage local comme -08:00:

Scala
java.time.ZoneId.systemDefault
res0:java.time.ZoneId = America/Los_Angeles
Scala
java.sql.Timestamp.valueOf("1883-11-10 00:00:00").getTimezoneOffset / 60.0
res1: Double = 8.0

L'API Java 8 équivalente renvoie un résultat différent :

Scala
java.time.ZoneId.of("America/Los_Angeles").getRules.getOffset(java.time.LocalDateTime.parse("1883-11-10T00:00:00"))
res2: java.time.ZoneOffset = -07:52:58

Avant le 18 novembre 1883, l'heure de la journée en Amérique du Nord était une affaire locale, et la plupart des villes et villages utilisaient une forme de temps solaire local, maintenue par une horloge bien connue (sur un clocher d'église, par exemple, ou dans la vitrine d'un bijoutier). C'est pourquoi vous voyez un décalage horaire si étrange.

L'exemple démontre que les fonctions Java 8 sont plus précises et prennent en compte les données historiques d'IANA TZDB. Après le passage à l'API de temps Java 8, Databricks Runtime 7.0 a bénéficié automatiquement de l'amélioration et est devenu plus précis quant à la manière dont il résout les décalages de fuseau horaire.

Databricks Runtime 7.0 est également passé au calendrier grégorien proleptique pour le type Timestamp. La norme ISO SQL:2016 déclare que la plage valide pour les Timestamp est de 0001-01-01 00:00:00 à 9999-12-31 23:59:59.999999. Databricks Runtime 7.0 est entièrement conforme à la norme et prend en charge tous les Timestamp de cette plage. Par rapport à Databricks Runtime 6.x et versions antérieures, notez les sous-plages suivantes :

  • 0001-01-01 00:00:00..1582-10-03 23:59:59.999999Databricks Runtime 6.x et les versions inférieures utilisent le calendrier julien et ne sont pas conformes à la norme. Databricks Runtime 7.0 corrige le problème et applique le calendrier grégorien proleptique dans les opérations internes sur les Timestamp, telles que l'obtention de l'année, du mois, du jour, etc. En raison de calendriers différents, certaines dates qui existent dans Databricks Runtime 6.x et versions antérieures n'existent pas dans Databricks Runtime 7.0. Par exemple, le 1000-02-29 n'est pas une date valide car 1000 n'est pas une année bissextile dans le calendrier grégorien. De plus, Databricks Runtime 6.x et les versions inférieures résolvent incorrectement le nom du fuseau horaire en décalages de fuseau pour cette plage de Timestamp.
  • 1582-10-04 00:00:00..1582-10-14 23:59:59.999999. Il s’agit d’une plage valide d’horodatages locaux dans Databricks Runtime 7.0, contrairement à Databricks Runtime 6.x et aux versions antérieures, où de tels Timestamp n’existaient pas.
  • 1582-10-15 00:00:00..1899-12-31 23:59:59.999999Databricks Runtime 7.0 résout correctement les décalages de fuseau horaire en utilisant les données historiques d'IANA TZDB. Comparé à Databricks Runtime 7.0, Databricks Runtime 6.x et les versions antérieures pourraient résoudre incorrectement les décalages de fuseau horaire à partir des noms de fuseaux horaires dans certains cas, comme illustré dans l'exemple précédent.
  • 1900-01-01 00:00:00..2036-12-31 23:59:59.999999. Les versions de Databricks Runtime 7.0 et Databricks Runtime 6.x et antérieures sont toutes deux conformes à la norme ANSI SQL et utilisent le calendrier grégorien dans les opérations de date et d'heure, comme l'obtention du jour du mois.
  • 2037-01-01 00:00:00..9999-12-31 23:59:59.999999. Databricks Runtime 6.x et versions antérieures peuvent résoudre incorrectement les décalages de fuseau horaire et les décalages d'heure d'été. Databricks Runtime 7.0 ne le fait pas.

Un autre aspect du mappage des noms de fuseaux horaires aux décalages est le chevauchement des locaux Timestamp qui peut se produire en raison de l'heure d'été (DST) ou du passage à un autre décalage de fuseau horaire standard. Par exemple, le 3 novembre 2019, à 02:00:00, la plupart des États des États-Unis ont reculé les horloges d'une heure, à 01:00:00. Le Timestamp local 2019-11-03 01:30:00 America/Los_Angeles peut être mappé soit à 2019-11-03 01:30:00 UTC-08:00, soit à 2019-11-03 01:30:00 UTC-07:00. Si vous ne spécifiez pas le décalage et définissez simplement le nom du fuseau horaire (par exemple, 2019-11-03 01:30:00 America/Los_Angeles), Databricks Runtime 7.0 utilise le décalage antérieur, correspondant généralement à l'heure « d'été ». Le comportement diffère de Databricks Runtime 6.x et versions antérieures qui utilisent le décalage « d'hiver ». En cas d'interruption, lorsque les horloges avancent, il n'y a aucun décalage valide. Pour un changement typique d'une heure d'heure d'été, Spark déplace ces Timestamps vers le prochain Timestamp valide correspondant à l'heure « d'été ».

Comme vous pouvez le constater à partir des exemples précédents, la correspondance des noms de fuseaux horaires aux décalages est ambiguë et n'est pas un à un. Dans les cas où cela est possible, lors de la construction des Timestamp, nous recommandons de spécifier des décalages horaires exacts, par exemple 2019-11-03 01:30:00 UTC-07:00.

ANSI SQL et Spark SQL Timestamp

La norme ANSI SQL définit deux types de Timestamp :

  • TIMESTAMP WITHOUT TIME ZONE ou TIMESTAMP: Local Timestamp as (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND). Ces horodatages ne sont liés à aucun fuseau horaire et sont des horodatages d'horloge murale.
  • TIMESTAMP WITH TIME ZONE: Timestamp zoné sous la forme (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, TIMEZONE_HOUR, TIMEZONE_MINUTE). Ces timestamps représentent un instant dans le fuseau horaire UTC + un décalage de fuseau horaire (en heures et minutes) associé à chaque valeur.

Le décalage de fuseau horaire d'un TIMESTAMP WITH TIME ZONE n'affecte pas le point temporel physique que le Timestamp représente, car il est entièrement représenté par l'instant de temps UTC donné par les autres composants du Timestamp. Au lieu de cela, le décalage de fuseau horaire n'affecte que le comportement par default d'une valeur de Timestamp pour l'affichage, l'extraction des composants de date/heure (par exemple, EXTRACT) et d'autres opérations qui nécessitent de connaître un fuseau horaire, comme l'ajout de mois à un Timestamp.

Spark SQL définit le type Timestamp comme TIMESTAMP WITH SESSION TIME ZONE, qui est une combinaison des champs (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, SESSION TZ) où le champ YEAR à SECOND identifie un instant de temps dans le fuseau horaire UTC, et où SESSION TZ est tiré de la configuration SQL spark.sql.session.timeZone. Le fuseau horaire de la session peut être défini comme :

  • Décalage de zone (+|-)HH:mm. Ce formulaire vous permet de définir de manière univoque un point physique dans le temps.
  • Nom du fuseau horaire sous la forme de l'ID de région area/city, tel que America/Los_Angeles. Ce type d’informations sur le fuseau horaire présente certains des problèmes décrits précédemment, tels que le chevauchement des timestamps locaux. Cependant, chaque instant UTC est associé de manière non ambiguë à un décalage de fuseau horaire pour tout ID de région et, par conséquent, chaque Timestamp avec un fuseau horaire basé sur un ID de région peut être converti de manière non ambiguë en un Timestamp avec un décalage de fuseau. Par default, le fuseau horaire de la session est défini sur le fuseau horaire par default de la machine virtuelle Java.

Spark TIMESTAMP WITH SESSION TIME ZONE est différent de :

  • TIMESTAMP WITHOUT TIME ZONE, car une valeur de ce type peut correspondre à plusieurs instants physiques, mais toute valeur de TIMESTAMP WITH SESSION TIME ZONE est un instant physique concret. Le type SQL peut être émulé en utilisant un décalage de fuseau horaire fixe pour toutes les sessions, par exemple UTC+0. Dans ce cas, vous pourriez considérer les Timestamps UTC comme des Timestamps locaux.
  • TIMESTAMP WITH TIME ZONE, car selon la norme SQL, les valeurs de colonne du type peuvent avoir des décalages de fuseau horaire différents. Cela n'est pas pris en charge par Spark SQL.

Vous remarquerez que les Timestamp associés à un fuseau horaire global (limité à la session) ne sont pas une nouveauté inventée par Spark SQL. Les SGBDR tels qu'Oracle fournissent un type similaire pour les Timestamp : TIMESTAMP WITH LOCAL TIME ZONE.

Construisez des dates et des Timestamp

Spark SQL fournit quelques méthodes pour construire des valeurs de date et de Timestamp :

  • Constructeurs par default sans paramètres : CURRENT_TIMESTAMP() et CURRENT_DATE().
  • À partir d'autres types primitifs Spark SQL, tels que INT, LONG et STRING
  • À partir de types externes comme les datetimes Python ou les classes Java java.time.LocalDate/Instant.
  • Désérialisation à partir de sources de données telles que CSV, JSON, Avro, Parquet, ORC, et ainsi de suite.

La fonction MAKE_DATE introduite dans Databricks Runtime 7.0 prend trois paramètres —YEAR, MONTH et DAY— et construit une valeur DATE. Tous les paramètres d'entrée sont implicitement convertis au type INT dans la mesure du possible. La fonction vérifie que les dates résultantes sont des dates valides dans le calendrier grégorien proleptique, sinon elle renvoie NULL. Par exemple :

Python
spark.createDataFrame([(2020, 6, 26), (1000, 2, 29), (-44, 1, 1)],['Y', 'M', 'D']).createTempView('YMD')
df = sql('select make_date(Y, M, D) as date from YMD')
df.printSchema()
root
|-- date: date (nullable = true)

Pour imprimer le contenu d'un DataFrame, appelez l'action show(), qui convertit les dates en chaînes de caractères sur les exécuteurs et transfère les chaînes au Driver pour les afficher sur la console :

Python
df.show()
+-----------+
| date|
+-----------+
| 2020-06-26|
| null|
|-0044-01-01|
+-----------+

De même, vous pouvez construire des valeurs de timestamp à l’aide des fonctions MAKE_TIMESTAMP. Comme MAKE_DATE, il effectue la même validation pour les champs de date et accepte en outre les champs de temps HEURE (0-23), MINUTE (0-59) et SECONDE (0-60). SECOND a le type Decimal(precision = 8, scale = 6) car les secondes peuvent être passées avec la partie fractionnaire jusqu'à la précision de la microseconde. Par exemple :

Python
df = spark.createDataFrame([(2020, 6, 28, 10, 31, 30.123456), \
(1582, 10, 10, 0, 1, 2.0001), (2019, 2, 29, 9, 29, 1.0)],['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND'])
df.show()
+----+-----+---+----+------+---------+
|YEAR|MONTH|DAY|HOUR|MINUTE| SECOND|
+----+-----+---+----+------+---------+
|2020| 6| 28| 10| 31|30.123456|
|1582| 10| 10| 0| 1| 2.0001|
|2019| 2| 29| 9| 29| 1.0|
+----+-----+---+----+------+---------+
Python
df.selectExpr("make_timestamp(YEAR, MONTH, DAY, HOUR, MINUTE, SECOND) as MAKE_TIMESTAMP")
ts.printSchema()
root
|-- MAKE_TIMESTAMP: timestamp (nullable = true)

En ce qui concerne les dates, imprimez le contenu du DataFrame ts à l'aide de l'action show(). De la même manière, show() convertit les Timestamp en chaînes de caractères, mais prend désormais en compte le fuseau horaire de session défini par la configuration SQL spark.sql.session.timeZone.

Python
ts.show(truncate=False)
+--------------------------+
|MAKE_TIMESTAMP |
+--------------------------+
|2020-06-28 10:31:30.123456|
|1582-10-10 00:01:02.0001 |
|null |
+--------------------------+

Spark ne peut pas créer le dernier timestamp, car cette date n’est pas valide : 2019 n’est pas une année bissextile.

Vous remarquerez qu’il n’y a pas d’informations de fuseau horaire dans l’exemple précédent. Dans ce cas, Spark prend un fuseau horaire de la configuration SQL spark.sql.session.timeZone et l’applique aux invocations de fonction. Vous pouvez également choisir un fuseau horaire différent en le transmettant comme dernier paramètre de MAKE_TIMESTAMP. Voici un exemple :

Python
df = spark.createDataFrame([(2020, 6, 28, 10, 31, 30, 'UTC'),(1582, 10, 10, 0, 1, 2, 'America/Los_Angeles'), \
(2019, 2, 28, 9, 29, 1, 'Europe/Moscow')], ['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND', 'TZ'])
df = df.selectExpr('make_timestamp(YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, TZ) as MAKE_TIMESTAMP')
df = df.selectExpr("date_format(MAKE_TIMESTAMP, 'yyyy-MM-dd HH:mm:ss VV') AS TIMESTAMP_STRING")
df.show(truncate=False)
+---------------------------------+
|TIMESTAMP_STRING |
+---------------------------------+
|2020-06-28 13:31:00 Europe/Moscow|
|1582-10-10 10:24:00 Europe/Moscow|
|2019-02-28 09:29:00 Europe/Moscow|
+---------------------------------+

Comme l'exemple le démontre, Spark tient compte des fuseaux horaires spécifiés, mais ajuste tous les Timestamp locaux au fuseau horaire de la session. Les fuseaux horaires d'origine transmis à la fonction MAKE_TIMESTAMP sont perdus, car le type TIMESTAMP WITH SESSION TIME ZONE suppose que toutes les valeurs appartiennent à un seul fuseau horaire, et il ne stocke même pas de fuseau horaire pour chaque valeur. Conformément à la définition de la TIMESTAMP WITH SESSION TIME ZONE, Spark stocke les Timestamp locaux dans le fuseau horaire UTC et utilise le fuseau horaire de la session lors de l'extraction de champs de date/heure ou de la conversion des Timestamp en chaînes de caractères.

De plus, les Timestamp peuvent être construits à partir du type LONG en utilisant le transtypage. Si une colonne LONG contient le nombre de secondes depuis l'époque 01.01.1970 00:00:00Z, elle peut être convertie en TIMESTAMP Spark SQL :

SQL
select CAST(-123456789 AS TIMESTAMP);
1966-02-02 05:26:51

Malheureusement, cette approche ne vous permet pas de spécifier la partie fractionnaire des secondes.

Une autre façon est de construire des dates et des Timestamp à partir de valeurs de type STRING. Vous pouvez créer des littéraux en utilisant des mots-clés spéciaux :

SQL
select timestamp '2020-06-28 22:17:33.123456 Europe/Amsterdam', date '2020-07-01';
2020-06-28 23:17:33.123456 2020-07-01

Vous pouvez également utiliser la conversion de type que vous pouvez appliquer à toutes les valeurs d'une colonne :

SQL
select cast('2020-06-28 22:17:33.123456 Europe/Amsterdam' as timestamp), cast('2020-07-01' as date);
2020-06-28 23:17:33.123456 2020-07-01

Les chaînes Timestamp d'entrée sont interprétées comme des Timestamp locaux dans le fuseau horaire spécifié ou dans le fuseau horaire de session si un fuseau horaire est omis dans la chaîne d'entrée. Les chaînes avec des modèles inhabituels peuvent être converties en Timestamp à l'aide de la fonction to_timestamp(). Les modèles pris en charge sont décrits dans Modèles de date et d'heure pour le formatage et l'analyse:

SQL
select to_timestamp('28/6/2020 22.17.33', 'dd/M/yyyy HH.mm.ss');
2020-06-28 22:17:33

Si vous ne spécifiez pas de modèle, la fonction se comporte de manière similaire à CAST.

Pour des raisons de convivialité, Spark SQL reconnaît des valeurs de chaîne de caractères spéciales dans toutes les méthodes qui acceptent une chaîne de caractères et renvoient un Timestamp ou une date :

  • epoch est un alias pour la date 1970-01-01 ou le Timestamp 1970-01-01 00:00:00Z.
  • now est le timestamp ou la date actuelle du fuseau horaire de la session. Dans une seule query, elle produit toujours le même résultat.
  • today est le début de la date actuelle pour le type TIMESTAMP ou juste la date actuelle pour le type DATE.
  • tomorrow est le début du jour suivant pour les Timestamp ou simplement le jour suivant pour le type DATE.
  • yesterday est le jour précédent celui en cours ou son début pour le type TIMESTAMP.

Par exemple :

SQL
select timestamp 'yesterday', timestamp 'today', timestamp 'now', timestamp 'tomorrow';
2020-06-27 00:00:00 2020-06-28 00:00:00 2020-06-28 23:07:07.18 2020-06-29 00:00:00
select date 'yesterday', date 'today', date 'now', date 'tomorrow';
2020-06-27 2020-06-28 2020-06-28 2020-06-29

Spark vous permet de créer Datasets à partir de collections existantes d'objets externes côté Driver et de créer des colonnes de types correspondants. Spark convertit les instances de types externes en représentations internes sémantiquement équivalentes. Par exemple, pour créer un Dataset avec DATE et TIMESTAMP colonnes à partir de collections Python, vous pouvez utiliser :

Python
import datetime
df = spark.createDataFrame([(datetime.datetime(2020, 7, 1, 0, 0, 0), datetime.date(2020, 7, 1))], ['timestamp', 'date'])
df.show()
+-------------------+----------+
| timestamp| date|
+-------------------+----------+
|2020-07-01 00:00:00|2020-07-01|
+-------------------+----------+

PySpark convertit les objets date-heure de Python en représentations internes de Spark SQL côté Driver en utilisant le fuseau horaire du système, qui peut être différent du paramètre de fuseau horaire de session de Spark spark.sql.session.timeZone. Les valeurs internes ne contiennent pas d'informations sur le fuseau horaire d'origine. Les futures opérations sur les valeurs de date et de Timestamp parallélisées ne prennent en compte que le fuseau horaire des sessions Spark SQL conformément à la définition de type TIMESTAMP WITH SESSION TIME ZONE.

De même, Spark reconnaît les types suivants comme des types de date-heure externes dans les API Java et Scala :

  • java.sql.Date et java.time.LocalDate comme types externes pour le type DATE
  • java.sql.Timestamp et java.time.Instant pour le type TIMESTAMP.

Il existe une différence entre les types java.sql.* et java.time.*. java.time.LocalDate et java.time.Instant ont été ajoutés dans Java 8, et les types sont basés sur le calendrier grégorien proleptique – le même calendrier que celui utilisé par Databricks Runtime 7.0 et versions ultérieures. java.sql.Date et java.sql.Timestamp ont un autre calendrier en dessous – le calendrier hybride (julien + grégorien depuis le 15-10-15), qui est le même que le calendrier hérité utilisé par Databricks Runtime 6.x et versions antérieures. En raison de différents systèmes de calendrier, Spark doit effectuer des opérations supplémentaires lors des conversions en représentations internes de Spark SQL et rebaser les dates/Timestamp d'entrée d'un calendrier à l'autre. L'opération de rebase a une petite surcharge pour les timestamps modernes après l'année 1900, et elle peut être plus significative pour les anciens timestamps.

L'exemple suivant montre comment créer des Timestamp à partir de collections Scala. Le premier exemple construit un objet java.sql.Timestamp à partir d'une chaîne. La méthode valueOf interprète les chaînes d'entrée comme un Timestamp local dans le fuseau horaire JVM par default, ce qui peut être différent du fuseau horaire de session de Spark. Si vous devez construire des instances de java.sql.Timestamp ou java.sql.Date dans un fuseau horaire spécifique, consultez java.text.SimpleDateFormat (et sa méthode setTimeZone) ou java.util.Calendar.

Scala
Seq(java.sql.Timestamp.valueOf("2020-06-29 22:41:30"), new java.sql.Timestamp(0)).toDF("ts").show(false)
+-------------------+
|ts |
+-------------------+
|2020-06-29 22:41:30|
|1970-01-01 03:00:00|
+-------------------+
Scala
Seq(java.time.Instant.ofEpochSecond(-12219261484L), java.time.Instant.EPOCH).toDF("ts").show
+-------------------+
| ts|
+-------------------+
|1582-10-15 11:12:13|
|1970-01-01 03:00:00|
+-------------------+

De même, vous pouvez créer une colonne DATE à partir de collections de java.sql.Date ou java.sql.LocalDate. La parallélisation des instances java.sql.LocalDate est entièrement indépendante de la session Spark ou des fuseaux horaires par default de la JVM, mais ce n'est pas le cas pour la parallélisation des instances java.sql.Date. Il y a des nuances :

  1. java.sql.Date les instances représentent les dates locales au fuseau horaire JVM default sur le Driver.
  2. Pour des conversions correctes vers des valeurs Spark SQL, le fuseau horaire JVM default sur le driver et les exécuteurs doit être le même.
Scala
Seq(java.time.LocalDate.of(2020, 2, 29), java.time.LocalDate.now).toDF("date").show
+----------+
| date|
+----------+
|2020-02-29|
|2020-06-29|
+----------+

Pour éviter tout problème lié au calendrier et au fuseau horaire, nous recommandons les types Java 8 java.sql.LocalDate/Instant comme types externes dans la parallélisation des collections Java/Scala de Timestamp ou de dates.

Collecter des dates et des Timestamp

L'opération inverse de la parallélisation consiste à collecter les dates et les Timestamp des exécuteurs vers le Driver et à renvoyer une collection de types externes. Pour l'exemple ci-dessus, vous pouvez récupérer le DataFrame vers le Driver à l'aide de l'action collect() :

Scala
df.collect()
[Row(timestamp=datetime.datetime(2020, 7, 1, 0, 0), date=datetime.date(2020, 7, 1))]

Spark transfère les valeurs internes des colonnes de dates et de timestamps comme des instants temporels dans le fuseau horaire UTC des exécuteurs vers le driver, et effectue des conversions en objets datetime Python dans le fuseau horaire système au niveau du driver, sans utiliser le fuseau horaire de session Spark SQL. collect() est différente de l'action show() décrite dans la section précédente. show() utilise le fuseau horaire de la session lors de la conversion des Timestamp en chaînes de caractères, et collecte les chaînes résultantes sur le Driver.

Dans les API Java et Scala, Spark effectue les conversions suivantes par default :

  • Les valeurs DATE de Spark SQL sont converties en instances de java.sql.Date.
  • Les valeurs TIMESTAMP de Spark SQL sont converties en instances de java.sql.Timestamp.

Les deux conversions sont effectuées dans le fuseau horaire JVM par default sur le Driver. Ainsi, pour avoir les mêmes champs de date-heure que ceux que vous pouvez obtenir en utilisant Date.getDay(), getHour(), et ainsi de suite, et en utilisant les fonctions Spark SQL DAY, HOUR, le fuseau horaire JVM par default sur le Driver et le fuseau horaire de session sur les exécuteurs devraient être identiques.

De même que pour la création de dates/Timestamp à partir de java.sql.Date/Timestamp, Databricks Runtime 7.0 effectue un rebasage du calendrier grégorien proleptique vers le calendrier hybride (julien + grégorien). Cette opération est presque gratuite pour les dates modernes (après l'année 1582) et les horodatages (après l'année 1900), mais elle pourrait entraîner des frais supplémentaires pour les dates et horodatages anciens.

Vous pouvez éviter de tels problèmes liés au calendrier et demander à Spark de renvoyer les types java.time, qui ont été ajoutés depuis Java 8. Si vous définissez la configuration SQL spark.sql.datetime.java8API.enabled sur vrai, l'action Dataset.collect() renvoie :

  • java.time.LocalDate pour le type DATE de Spark SQL
  • java.time.Instant pour le type TIMESTAMP de Spark SQL

Désormais, les conversions ne souffrent plus des problèmes liés au calendrier, car les types Java 8 et Databricks Runtime 7.0 et versions ultérieures sont tous deux basés sur le calendrier grégorien proleptique. L'action collect() ne dépend pas du fuseau horaire JVM default. Les conversions de timestamp ne dépendent pas du tout du fuseau horaire. Les conversions de date utilisent le fuseau horaire de session à partir de la configuration SQL spark.sql.session.timeZone. Par exemple, considérez un Dataset avec des colonnes DATE et TIMESTAMP, avec le fuseau horaire JVM par default défini sur Europe/Moscow et le fuseau horaire de session défini sur America/Los_Angeles.

Scala
java.util.TimeZone.getDefault
res1: java.util.TimeZone = sun.util.calendar.ZoneInfo[id="Europe/Moscow",...]
Scala
spark.conf.get("spark.sql.session.timeZone")
res2: String = America/Los_Angeles
Scala
df.show
+-------------------+----------+
| timestamp| date|
+-------------------+----------+
|2020-07-01 00:00:00|2020-07-01|
+-------------------+----------+

L’action show() imprime le timestamp à l’heure de la session America/Los_Angeles, mais si vous collectez le Dataset, il est converti en java.sql.Timestamp et la méthode toString imprime Europe/Moscow:

Scala
df.collect()
res16: Array[org.apache.spark.sql.Row] = Array([2020-07-01 10:00:00.0,2020-07-01])
Scala
df.collect()(0).getAs[java.sql.Timestamp](0).toString
res18: java.sql.Timestamp = 2020-07-01 10:00:00.0

En fait, le timestamp local 2020-07-01 00:00:00 correspond à 2020-07-01T07:00:00Z en UTC. Vous pouvez observer que si vous activez l'API Java 8 et collectez le Dataset :

Scala
df.collect()
res27: Array[org.apache.spark.sql.Row] = Array([2020-07-01T07:00:00Z,2020-07-01])

Vous pouvez convertir un objet java.time.Instant en n'importe quel Timestamp local indépendamment du fuseau horaire JVM global. C'est l'un des avantages de java.time.Instant par rapport à java.sql.Timestamp. Le premier nécessite de modifier le paramètre JVM global, ce qui influence les autres Timestamp sur la même JVM. Par conséquent, si vos applications traitent des dates ou des Timestamp dans différents fuseaux horaires, et que les applications ne doivent pas entrer en conflit les unes avec les autres lors de la collecte de données vers le Driver à l'aide de l'API Java ou Scala Dataset.collect(), nous vous recommandons de passer à l'API Java 8 en utilisant la configuration SQL spark.sql.datetime.java8API.enabled.