Passer au contenu principal
Date, Time et Timestamp nécessitent une attention particulière, car plusieurs problèmes courants y sont liés. Le problème le plus fréquent est la gestion des fuseaux horaires. Un autre concerne leur représentation sous forme de chaîne et la manière de l’utiliser. En outre, chaque base de données et chaque pilote ont leurs propres spécificités et limites. Ce document a pour objectif de servir de guide d’aide à la décision en décrivant les tâches, en fournissant des détails d’implémentation et en expliquant les problèmes.

Fuseaux horaires

Nous savons tous que les fuseaux horaires sont difficiles à gérer (heure d’été, changements constants de décalage). Mais cette section traite d’un autre problème lié aux fuseaux horaires : leur rapport à la représentation textuelle des horodatages.

Comment ClickHouse convertit les chaînes DateTime

ClickHouse utilise les règles suivantes pour convertir les valeurs de chaîne DateTime :
  • Si une colonne est définie avec un fuseau horaire (DateTime64(9, ‘Asia/Tokyo’)), la valeur de chaîne est alors interprétée comme un timestamp dans ce fuseau horaire. 2026-01-01 13:00:00 correspondra à 2026-01-01 04:00:00 en UTC.
  • Si une colonne n’a pas de définition de fuseau horaire, seul le fuseau horaire du serveur est utilisé. Important : le paramètre session_timezone n’a aucun effet. Ainsi, si le fuseau horaire du serveur est UTC et celui de la session America/Los_Angeles, 2026-01-01 13:00:00 sera écrit en UTC.
  • Lorsqu’une valeur est lue depuis une colonne sans définition de fuseau horaire, session_timezone est utilisé ou, s’il n’est pas défini, le fuseau horaire du serveur. C’est pourquoi la lecture des timestamps sous forme de chaînes peut être affectée par session_timezone. Il n’y a rien d’anormal à cela, mais il faut le garder à l’esprit.

Écriture de timestamps entre fuseaux horaires

Supposons maintenant qu’une application s’exécute dans la région us-west, avec le fuseau horaire local UTC-8, et que nous devions écrire un timestamp local 2026-01-01 02:00:00, qui correspond en UTC à 2026-01-01 10:00:00 :
  • L’écrire sous forme de chaîne nécessite de le convertir dans le fuseau horaire du serveur ou de la colonne.
  • L’écrire sous forme de structure temporelle native au langage nécessite que le pilote connaisse le fuseau horaire cible, mais :
    • Ce n’est pas toujours possible
    • L’API du pilote est mal conçue pour cela
    • La seule solution consiste à décrire les transformations qui seront appliquées afin que l’application puisse compenser (ou écrire un timestamp Unix sous forme numérique)

API de timestamp Java et JDBC

Java et JDBC offrent différentes façons de définir un timestamp :
  1. Utiliser la classe Timestamp, qui correspond en réalité à un timestamp Unix.
    1. Lorsqu’elle est utilisée avec un objet Calendar, elle permet de réinterpréter le Timestamp dans le fuseau horaire du calendrier.
    2. Timestamp possède un calendrier interne peu évident.
  2. Utiliser la classe LocalDateTime, qui se convertit facilement vers n’importe quel fuseau horaire, mais aucune méthode ne permet de lui passer un fuseau horaire cible.
  3. Utiliser la classe ZonedDateTime, qui facilite la conversion de fuseau horaire lors de l’écriture dans un DateTime sans fuseau horaire (puisque nous savons qu’il faut utiliser le fuseau horaire du serveur).
    1. En revanche, écrire un ZonedDateTime dans une colonne avec un fuseau horaire défini oblige l’utilisateur à compenser la conversion du pilote.
  4. Utiliser Long pour écrire des millisecondes de timestamp Unix.
  5. Utiliser String pour effectuer toutes les conversions côté application (ce qui n’est pas très portable).
Préférez java.time.ZoneId#of(java.lang.String) lorsque vous recherchez un fuseau horaire par ID. Cette méthode déclenchera une exception si le fuseau horaire est introuvable (java.util.TimeZone#getTimeZone(java.lang.String) reviendra silencieusement à GMT).La bonne façon d’obtenir le fuseau horaire Tokyo est :TimeZone.getTimeZone(ZoneId.of("Asia/Tokyo"))

Date

Par nature, les dates sont indépendantes des fuseaux horaires. Les types Date et Date32 permettent de stocker des dates. Tous deux utilisent un nombre de jours écoulés depuis l’epoch (1970-01-01). Date n’utilise que des nombres de jours positifs, sa plage se terminant donc au 2149-06-06. Date32 gère les nombres de jours négatifs pour couvrir les dates antérieures au 1970-01-01, mais sa plage est plus restreinte (du 1900-01-01 au 2100-01-01, où 0 correspond à 1970-01-01). ClickHouse considère 2026-01-01 comme 2026-01-01 quel que soit le fuseau horaire, et il n’existe pas de paramètre de fuseau horaire dans les définitions de colonnes.

Utilisation de java.time.LocalDate

En Java, la classe la plus adaptée pour représenter des dates est java.time.LocalDate. Le client utilise cette classe pour stocker la valeur des colonnes Date et Date32 (lecture de LocalDate.ofEpochDay((long)readUnsignedShortLE())). Nous recommandons d’utiliser java.time.LocalDate, car cette classe n’est pas affectée par les changements de fuseau horaire et fait partie de l’API moderne de gestion du temps.

Utilisation de java.sql.Date

LocalDate a été introduit dans Java 8. Auparavant, java.sql.Date était utilisé pour écrire/lire des dates. En interne, cette classe est un wrapper autour d’un instant (une valeur temporelle représentant un point absolu dans le temps). De ce fait, toString() renvoie une date différente selon le fuseau horaire de la JVM. Cela oblige le pilote à construire soigneusement les valeurs et demande à l’utilisateur d’en être conscient.

Réinterprétation basée sur le calendrier

java.sql.ResultSet possède une méthode pour obtenir des valeurs de date qui accepte un Calendar, et il existe une méthode similaire dans java.sql.PreparedStatement. Cela a été conçu pour permettre au ClickHouse JDBC pilote de réinterpréter une valeur de date dans le fuseau horaire spécifié. Par exemple, la DB contient la valeur 2026-01-01, mais l’application veut interpréter cette date comme minuit à Tokyo. Cela signifie que l’objet java.sql.Date renvoyé se verra attribuer un instant précis et, une fois converti dans le fuseau horaire local, il pourra correspondre à une date différente en raison du décalage horaire. On peut obtenir le même résultat avec LocalDate en utilisant java.time.LocalDate#atStartOfDay(java.time.ZoneId). Le ClickHouse JDBC pilote renvoie toujours un objet java.sql.Date qui correspond à la date locale à minuit. En d’autres termes, si la date est 2026-01-01, cela signifie 2026-01-01 12:00 AM dans le fuseau horaire de la JVM (le même comportement que les pilotes JDBC PostgreSQL et MariaDB).

Time

Les valeurs Time, comme les valeurs Date, sont dans la plupart des cas indépendantes du fuseau horaire. ClickHouse n’effectue aucune transformation des valeurs littérales d’heure vers un quelconque fuseau horaire — ’6:30’ reste identique, quel que soit l’endroit où il est lu.

Types Time de ClickHouse

Time et Time64 ont été introduits dans 25.6. Auparavant, on utilisait à la place les types d’horodatage DateTime et DateTime64 (abordés plus loin dans ce guide). Time est stocké sous la forme d’un entier 32 bits représentant un nombre de secondes, dans la plage [-999:59:59, 999:59:59]. Time64 est encodé sous la forme d’un Decimal64 non signé et stocke différentes unités de temps selon la précision. Les valeurs courantes sont 3 (millisecondes), 6 (microsecondes) et 9 (nanosecondes). La plage des valeurs de précision est [0, 9].

Correspondance des types Java

Le client lit Time et Time64 et les stocke sous forme de LocalDateTime. Ce choix permet de prendre en charge les intervalles de temps négatifs (LocalTime ne les prend pas en charge). Dans ce cas, la partie date correspond à la date Epoch 1970-01-01 ; les valeurs négatives seront donc antérieures à cette date. La prise en charge principale des types temporels repose sur LocalTime (lorsque la valeur tient sur une journée) et sur Duration pour couvrir toute la plage de valeurs. LocalDateTime ne peut être utilisé qu’en lecture.

Utilisation de java.sql.Time

L’utilisation de java.sql.Time est limitée à l’intervalle de LocalTime. En interne, java.sql.Time est converti en chaîne littérale. La valeur peut être modifiée à l’aide d’un paramètre Calendar avec PreparedStatement#setTime().

La fonction toTime

Timestamp

Un timestamp correspond à un instant précis. Par exemple, un timestamp Unix représente n’importe quel instant sous la forme d’un nombre de secondes écoulées depuis 1970-01-01 00:00:00 UTC (un nombre de secondes négatif représente un timestamp antérieur à l’époque Unix, et un nombre positif un timestamp postérieur). Cette représentation est facile à calculer et à manipuler si l’observateur se trouve dans le fuseau horaire UTC ou l’utilise à la place de son fuseau local.

Types de timestamp ClickHouse

Dans ClickHouse, il existe les types de timestamp DateTime (entier 32 bits, résolution toujours en secondes) et DateTime64 (entier 64 bits, dont la résolution dépend de la définition). Les valeurs sont toujours stockées sous forme de timestamps UTC. Cela signifie que, lorsqu’elles sont représentées sous forme de nombres, aucune conversion de fuseau horaire n’est appliquée.

Représentation sous forme de chaîne et comportement du fuseau horaire

La représentation sous forme de chaîne présente certaines subtilités :
  • Si aucun fuseau horaire n’est spécifié dans la définition de la colonne et qu’une chaîne est fournie lors de l’écriture, elle sera convertie du fuseau horaire du serveur en un timestamp UTC numérique. Lorsqu’une valeur est lue depuis une telle colonne, elle est convertie d’un timestamp UTC en un timestamp littéral à l’aide du fuseau horaire du serveur ou de la session (une approche similaire s’applique aux timestamps littéraux dans les expressions où le fuseau horaire n’est pas explicitement défini).
  • Si un fuseau horaire est spécifié dans la définition de la colonne, alors seul ce fuseau horaire est utilisé pour toutes les conversions de chaînes. Cela diffère de la logique appliquée lorsqu’aucun fuseau horaire n’est spécifié, ce qui exige de bien comprendre comment les données sont écrites pour chaque colonne de la requête.
  • Si une date est fournie sous forme de chaîne dans un format qui inclut un fuseau horaire, une fonction de conversion est nécessaire. On utilise généralement parseDateTimeBestEffort.

Comment le pilote JDBC gère les timestamps

Dans le pilote JDBC, nous convertissons les timestamps en représentation numérique :
"fromUnixTimestamp64Nano(" + epochSeconds * 1_000_000_000L + nanos + ")"
Cette représentation résout la plupart des problèmes de conversion liés aux valeurs de timestamp, car elle envoie les données au serveur dans un format unifié. Cette approche nécessite toutefois un léger ajustement des instructions SQL, mais elle offre le moyen le plus simple et le plus direct d’écrire des timestamps dans n’importe quelle colonne. DateTime et DateTime64 sont lus et stockés côté client sous forme de java.time.ZonedDateTime, ce qui permet de convertir ces valeurs vers n’importe quel autre fuseau horaire (les informations de fuseau horaire sont conservées).

Piège courant avec toDateTime64

L’exemple de code suivant semble correct, mais l’assertion échoue :
String sql = "SELECT toDateTime64(?, 3)";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
    LocalDateTime localTs = LocalDateTime.parse("2021-01-01T01:34:56");
    stmt.setObject(1, localTs);
    try (ResultSet rs = stmt.executeQuery()) {
        rs.next();
        assertEquals(rs.getObject(1, LocalDateTime.class), localTs);
    }
}
Cela se produit parce que toDateTime64 utilise le fuseau horaire du serveur et ne tient pas compte du fuseau horaire d’origine.

Tables de conversion

Si une paire de conversion n’est pas mentionnée dans les tableaux ci-dessous, la conversion n’est pas prise en charge. Par exemple, les colonnes Date ne peuvent pas être lues comme des java.sql.Timestamp, car elles ne comportent pas de composante horaire. Le pilote ne convertit pas les valeurs entières en valeurs de date/heure. L’appel à pstmt.setLong("timestamp", 1772132359L) entraîne l’écriture de 1772132359 en tant que nombre sur le serveur, où il sera interprété comme un timestamp Unix UTC en secondes.

Écriture de valeurs avec PreparedStatement#setObject

Le tableau suivant indique comment les valeurs sont converties lorsqu’elles sont passées à PreparedStatement#setObject(column, value) :
Classe de valueConversion
java.time.LocalDateFormatée au format YYYY-MM-DD.
java.sql.DateConvertie avec le calendrier par défaut et formatée en LocalDate (YYYY-MM-DD).
java.time.LocalTimeFormatée au format HH:mm:ss.
java.time.DurationFormatée au format HHH:mm:ss. La valeur peut être négative.
java.sql.TimeConvertie avec le calendrier par défaut et formatée en LocalTime (HH:mm).
java.time.LocalDateTimeConvertie en timestamp Unix en nanosecondes et enveloppée dans fromUnixTimestamp64Nano.
java.time.ZonedDateTimeConvertie en timestamp Unix en nanosecondes et enveloppée dans fromUnixTimestamp64Nano.
java.sql.TimestampConvertie en timestamp Unix en nanosecondes et enveloppée dans fromUnixTimestamp64Nano.
Le type de la colonne doit être considéré comme inconnu. C’est à l’application de décider ce qu’elle doit transmettre à l’instruction préparée.

Lecture des valeurs avec ResultSet#getObject

Le tableau suivant montre comment les valeurs sont converties lorsqu’elles sont lues avec ResultSet#getObject(column, class) :
Type de données ClickHouse de columnValeur de classConversion
Date ou Date32java.time.LocalDateValeur de la base de données (nombre de jours) convertie en LocalDate.
Date ou Date32java.sql.DateValeur de la base de données (nombre de jours) convertie en LocalDate, puis en java.sql.Date en prenant minuit dans le fuseau horaire local comme partie horaire. Si un calendrier est utilisé, son fuseau horaire sera utilisé à la place du fuseau local. Exemple : valeur de la base de données 1970-01-10LocalDate vaut 1970-01-10.
Time ou Time64java.time.LocalTimeValeur de la base de données convertie en LocalDateTime, puis en LocalTime. Cela fonctionne uniquement pour une heure de la journée.
Time ou Time64java.time.LocalDateTimeValeur de la base de données convertie en LocalDateTime.
Time ou Time64java.sql.TimeValeur de la base de données convertie en LocalDateTime, puis en java.sql.Time à l’aide du calendrier par défaut. Cela fonctionne uniquement pour une heure de la journée.
Time ou Time64java.time.DurationValeur de la base de données convertie en LocalDateTime, puis en Duration.
DateTime ou DateTime64java.time.LocalDateTimeValeur de la base de données convertie en ZonedDateTime, puis en LocalDateTime.
DateTime ou DateTime64java.time.ZonedDateTimeValeur de la base de données convertie en ZonedDateTime.
DateTime ou DateTime64java.sql.TimestampValeur de la base de données convertie en ZonedDateTime, puis en java.sql.Timestamp à l’aide du fuseau horaire par défaut.

Utilisation des méthodes fondées sur le calendrier

Utilisez ResultSet#getTime(column, calendar) et ResultSet#getDate(column, calendar) si les valeurs ont été stockées respectivement à l’aide de PreparedStatement#setTime(param, value, calendar) et PreparedStatement#setDate(param, value, calendar).
Dernière modification le 25 juin 2026