Saltar al contenido principal
Date, Time y Timestamp requieren especial atención porque hay varios problemas habituales relacionados con ellos. El problema más común es cómo gestionar las zonas horarias. Otro problema es su representación en cadena y cómo utilizarla. Además, cada base de datos y driver tiene sus propias particularidades y limitaciones. Este documento pretende servir como guía para la toma de decisiones mediante la descripción de tareas, la presentación de detalles de implementación y la explicación de los problemas.

Zonas horarias

Todos sabemos que las zonas horarias son difíciles de gestionar (horario de verano, cambios constantes en los desfases). Pero esta sección trata de otro problema relacionado con las zonas horarias: cómo se relacionan con la representación textual de las marcas de tiempo.

Cómo ClickHouse convierte cadenas de DateTime

ClickHouse usa las siguientes reglas para convertir valores de cadena de DateTime:
  • Si una columna se define con una zona horaria (DateTime64(9, ‘Asia/Tokyo’)), el valor de cadena se tratará como una marca de tiempo en esa zona horaria. 2026-01-01 13:00:00 será 2026-01-01 04:00:00 en hora UTC.
  • Si una columna no tiene definida ninguna zona horaria, solo se usa la zona horaria del servidor. Importante: la configuración session_timezone no tiene ningún efecto. Por lo tanto, si la zona horaria del servidor es UTC y la zona horaria de la sesión es America/Los_Angeles, 2026-01-01 13:00:00 se escribirá como hora UTC.
  • Cuando se lee un valor de una columna sin una zona horaria definida, se usa session_timezone o, si no está configurado, la zona horaria del servidor. Por eso, la lectura de marcas de tiempo como cadenas puede verse afectada por session_timezone. No hay nada incorrecto en ello, pero conviene tenerlo en cuenta.

Escritura de timestamps en distintas zonas horarias

Ahora supongamos que tenemos una aplicación en ejecución en la región us-west con la zona horaria local UTC-8, y que necesitamos escribir un timestamp local 2026-01-01 02:00:00 que en UTC es 2026-01-01 10:00:00:
  • Escribirlo como una cadena requiere convertirlo a la zona horaria del servidor o de la columna.
  • Escribirlo como una estructura de tiempo nativa del lenguaje requiere que el driver conozca la zona horaria de destino, pero:
    • No siempre es posible
    • La API del driver no está bien diseñada para esto
    • La única manera es describir qué transformaciones se realizarán para que la aplicación pueda compensarlo (o escribir un Unix timestamp como un número)

Java y las API de timestamp de JDBC

Java y JDBC tienen distintas formas de establecer un timestamp:
  1. Usa la clase Timestamp, que en realidad es un Unix timestamp.
    1. Cuando se usa con un objeto Calendar, permite reinterpretar el Timestamp en la zona horaria del calendario.
    2. Timestamp tiene un calendario interno que no es muy evidente.
  2. Usa la clase LocalDateTime, que se puede convertir fácilmente a cualquier zona horaria, pero no existe ningún método que permita pasar una zona horaria de destino.
  3. Usa la clase ZonedDateTime, que ayuda con la conversión de zona horaria al escribir en un DateTime sin zona horaria (porque sabemos que debe usarse la zona horaria del servidor).
    1. Pero escribir un ZonedDateTime en una columna con una zona horaria definida requiere que el usuario compense la conversión del driver.
  4. Usa Long para escribir milisegundos de Unix timestamp.
  5. Usa String para realizar todas las conversiones del lado de la aplicación (lo cual no es muy portable).
Se recomienda usar java.time.ZoneId#of(java.lang.String) al buscar una zona horaria por ID. Este método lanzará una excepción si no se encuentra la zona horaria (java.util.TimeZone#getTimeZone(java.lang.String) volverá silenciosamente a GMT).La forma correcta de obtener la zona horaria Tokyo es:TimeZone.getTimeZone(ZoneId.of("Asia/Tokyo"))

Fecha

Las fechas son, por naturaleza, independientes de la zona horaria. Existen los tipos Date y Date32 para almacenar fechas. Ambos tipos usan un número de días desde la época (1970-01-01). Date usa únicamente números positivos de días, por lo que su rango termina en 2149-06-06. Date32 admite números negativos de días para cubrir fechas anteriores a 1970-01-01, pero su rango es más reducido (de 1900-01-01 a 2100-01-01, donde 0 es 1970-01-01). ClickHouse considera 2026-01-01 como 2026-01-01 en cualquier zona horaria, y no existe ningún parámetro de zona horaria para las definiciones de columnas.

Uso de java.time.LocalDate

En Java, la clase más adecuada para representar valores de fecha es java.time.LocalDate. El cliente utiliza esta clase para almacenar el valor de las columnas Date y Date32 (leyendo LocalDate.ofEpochDay((long)readUnsignedShortLE())). Recomendamos usar java.time.LocalDate porque no se ve afectada por las conversiones de zona horaria y forma parte de la API moderna de fecha y hora.

Uso de java.sql.Date

LocalDate se introdujo en Java 8. Antes de eso, se usaba java.sql.Date para escribir y leer fechas. Internamente, esta clase es un contenedor de un instante (un valor temporal que representa un punto absoluto en el tiempo). Debido a ello, toString() devuelve una fecha distinta según la zona horaria en la que se ejecute la JVM. Esto obliga al driver a construir los valores con cuidado y requiere que el usuario sea consciente de ello.

Reinterpretación basada en calendario

java.sql.ResultSet tiene un método para obtener valores de fecha que acepta un Calendar, y java.sql.PreparedStatement incluye un método similar. Esto se diseñó para permitir que el driver JDBC reinterprete un valor de fecha en la zona horaria especificada. Por ejemplo, la DB tiene el valor 2026-01-01, pero la aplicación quiere interpretar esta fecha como la medianoche en Tokyo. Esto significa que el objeto java.sql.Date devuelto tendrá un instante específico y, al convertirlo a la zona horaria local, puede corresponder a una fecha distinta debido a la diferencia horaria. Podemos lograr lo mismo con LocalDate mediante java.time.LocalDate#atStartOfDay(java.time.ZoneId). El driver JDBC de ClickHouse siempre devuelve un objeto java.sql.Date que apunta a la fecha local a medianoche. En otras palabras, si la fecha es 2026-01-01, nos referimos a 2026-01-01 12:00 AM en la zona horaria de la JVM (el mismo comportamiento que los drivers JDBC de PostgreSQL y MariaDB).

Time

Los valores de Time, al igual que los de Date, no dependen de la zona horaria en la mayoría de los casos. ClickHouse no realiza ninguna transformación de los valores literales de hora a ninguna zona horaria: ’6:30’ es el mismo en cualquier lugar donde se lea.

Tipos de tiempo de ClickHouse

Time y Time64 se introdujeron en la versión 25.6. Antes de eso, se usaban en su lugar los tipos de timestamp DateTime y DateTime64 (que se analizan más adelante en esta guía). Time se almacena como un entero de 32 bits que representa una cantidad de segundos, y su rango es [-999:59:59, 999:59:59]. Time64 se codifica como un Decimal64 sin signo y almacena distintas unidades de tiempo según la precisión. Las opciones habituales son 3 (milisegundos), 6 (microsegundos) y 9 (nanosegundos). El rango de valores de precisión es [0, 9].

Correspondencia de tipos de Java

El cliente lee Time y Time64 y los almacena como LocalDateTime. Esto se hace para admitir el rango de tiempos negativos (LocalTime no lo admite). En este caso, la parte de la fecha corresponde a la fecha de época 1970-01-01, por lo que los valores negativos quedarán antes de esa fecha. La compatibilidad principal con los tipos de tiempo se implementa mediante LocalTime (cuando el valor cabe dentro de un día) y Duration para abarcar todo el rango de valores. LocalDateTime solo puede usarse para lectura.

Uso de java.sql.Time

El uso de java.sql.Time está limitado al intervalo de LocalTime. Internamente, java.sql.Time se convierte en un literal de cadena. El valor puede modificarse usando un parámetro Calendar con PreparedStatement#setTime().

La función toTime

timestamp

Una timestamp es un punto específico en el tiempo. Por ejemplo, una timestamp Unix representa cualquier instante como un número de segundos relativo a 1970-01-01 00:00:00 UTC (un número negativo de segundos representa una timestamp anterior a la hora Unix, y un número positivo representa una posterior). Esta representación es fácil de calcular y manejar si el observador se encuentra en la zona horaria UTC o la usa en lugar de su zona horaria local.

Tipos de timestamp de ClickHouse

En ClickHouse existen los tipos de timestamp DateTime (entero de 32 bits; la resolución siempre es en segundos) y DateTime64 (entero de 64 bits; la resolución depende de la definición). Los valores siempre se almacenan como timestamps UTC. Esto significa que, cuando se representan como números, no se aplica ninguna conversión de zona horaria.

Representación en cadena y comportamiento de la zona horaria

La representación en cadena tiene ciertas complejidades:
  • Si no se especifica ninguna zona horaria en la definición de la columna y se pasa una cadena al escribir, se convertirá de la zona horaria del servidor a un número de timestamp UTC. Cuando se lee un valor de esa columna, se convertirá de un timestamp UTC a un timestamp literal usando la zona horaria del servidor o de la sesión (se aplica un enfoque similar a los literales de timestamp en expresiones donde la zona horaria no se define explícitamente).
  • Si se especifica una zona horaria en la definición de la columna, solo se usa esa zona horaria en todas las conversiones de cadenas. Esto contradice la lógica aplicada cuando no se especifica ninguna zona horaria, por lo que requiere comprender bien cómo se escriben los datos para cada columna de la consulta.
  • Si se pasa una fecha como cadena en un formato que incluye una zona horaria, se necesita una función de conversión. Normalmente se usa parseDateTimeBestEffort.

Cómo el driver JDBC gestiona los timestamps

En el controlador JDBC, convertimos los timestamps en una representación numérica:
"fromUnixTimestamp64Nano(" + epochSeconds * 1_000_000_000L + nanos + ")"
Esta representación resuelve la mayoría de los problemas de conversión de los valores de timestamp, ya que envía los datos al servidor en un formato unificado. Sin embargo, este enfoque requiere un pequeño ajuste en las Sentencias SQL, pero ofrece la forma más sencilla y directa de escribir timestamps en cualquier columna. DateTime y DateTime64 se leen y almacenan en el cliente como java.time.ZonedDateTime, lo que facilita la conversión de estos valores a cualquier otra zona horaria (se conserva la información de la zona horaria).

Error habitual con toDateTime64

El siguiente ejemplo de código parece correcto, pero falla en la aserción:
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);
    }
}
Esto ocurre porque toDateTime64 usa la zona horaria del servidor y no tiene en cuenta la zona horaria de origen.

Tablas de conversión

Si un par de conversión no aparece en las tablas de abajo, significa que esa conversión no es compatible. Por ejemplo, las columnas Date no pueden leerse como java.sql.Timestamp porque no tienen parte de hora. El driver no convierte valores enteros en ningún valor de fecha u hora. Llamar a pstmt.setLong("timestamp", 1772132359L) hará que 1772132359 se escriba como un número en el servidor, que se tratará como un Unix timestamp UTC en segundos.

Escritura de valores con PreparedStatement#setObject

La siguiente tabla muestra cómo se convierten los valores al establecerlos con PreparedStatement#setObject(column, value):
Clase de valueConversión
java.time.LocalDateSe formatea como YYYY-MM-DD.
java.sql.DateSe convierte con el calendario predeterminado y se formatea como LocalDate (YYYY-MM-DD).
java.time.LocalTimeSe formatea como HH:mm:ss.
java.time.DurationSe formatea como HHH:mm:ss. El valor puede ser negativo.
java.sql.TimeSe convierte con el calendario predeterminado y se formatea como LocalTime (HH:mm).
java.time.LocalDateTimeSe convierte en una timestamp Unix en nanosegundos y se envuelve con fromUnixTimestamp64Nano.
java.time.ZonedDateTimeSe convierte en una timestamp Unix en nanosegundos y se envuelve con fromUnixTimestamp64Nano.
java.sql.TimestampSe convierte en una timestamp Unix en nanosegundos y se envuelve con fromUnixTimestamp64Nano.
El tipo de la columna debe considerarse desconocido. La aplicación es la que debe decidir qué pasar a la sentencia preparada.

Lectura de valores con ResultSet#getObject

La siguiente tabla muestra cómo se convierten los valores cuando se leen con ResultSet#getObject(column, class):
Tipo de dato de ClickHouse de columnValor de classConversión
Date o Date32java.time.LocalDateValor de la DB (número de días) convertido a LocalDate.
Date o Date32java.sql.DateValor de la DB (número de días) convertido a LocalDate y luego a java.sql.Date, usando la medianoche de la zona horaria local como parte de la hora. Si se usa un calendario, se utilizará su zona horaria en lugar de la local. Ejemplo: valor de la DB 1970-01-10LocalDate es 1970-01-10.
Time o Time64java.time.LocalTimeValor de la DB convertido a LocalDateTime y luego a LocalTime. Esto solo funciona para horas dentro de un mismo día.
Time o Time64java.time.LocalDateTimeValor de la DB convertido a LocalDateTime.
Time o Time64java.sql.TimeValor de la DB convertido a LocalDateTime y luego a java.sql.Time usando el calendario predeterminado. Esto solo funciona para horas dentro de un mismo día.
Time o Time64java.time.DurationValor de la DB convertido a LocalDateTime y luego a Duration.
DateTime o DateTime64java.time.LocalDateTimeValor de la DB convertido a ZonedDateTime y luego a LocalDateTime.
DateTime o DateTime64java.time.ZonedDateTimeValor de la DB convertido a ZonedDateTime.
DateTime o DateTime64java.sql.TimestampValor de la DB convertido a ZonedDateTime y luego a java.sql.Timestamp usando la zona horaria predeterminada.

Uso de métodos basados en calendario

Use ResultSet#getTime(column, calendar) y ResultSet#getDate(column, calendar) si los valores se almacenaron con PreparedStatement#setTime(param, value, calendar) y PreparedStatement#setDate(param, value, calendar), respectivamente.
Última modificación el 10 de junio de 2026