Перейти к основному содержанию
Date, Time и Timestamp требуют особого внимания, поскольку с ними связано несколько распространённых проблем. Самая частая из них — работа с часовыми поясами. Ещё одна проблема — строковое представление и его использование. Кроме того, у каждой базы данных и драйвера есть свои особенности и ограничения. Этот документ задуман как руководство, помогающее принимать решения: в нём описаны задачи, приведены подробности реализации и объяснены возникающие проблемы.

Часовые пояса

Все мы знаем, что с часовыми поясами непросто (переход на летнее время, постоянные изменения смещения). Но этот раздел посвящён другой проблеме, связанной с часовыми поясами: тому, как они соотносятся со строковым представлением временных меток.

Как ClickHouse преобразует строки DateTime

ClickHouse использует следующие правила для преобразования строковых значений DateTime:
  • Если столбец определён с часовым поясом (DateTime64(9, ‘Asia/Tokyo’)), строковое значение будет интерпретироваться как временная метка в этом часовом поясе. 2026-01-01 13:00:00 будет соответствовать 2026-01-01 04:00:00 по времени UTC.
  • Если у столбца не задан часовой пояс, используется только часовой пояс сервера. Важно: настройка session_timezone не влияет. Поэтому, если часовой пояс сервера — UTC, а часовой пояс сеанса — America/Los_Angeles, то 2026-01-01 13:00:00 будет записано как время UTC.
  • Когда значение считывается из столбца без заданного часового пояса, используется session_timezone, а если он не задан — часовой пояс сервера. Поэтому на чтение временных меток в виде строк может влиять session_timezone. В этом нет ничего неправильного, но об этом следует помнить.

Запись временных меток в разных часовых поясах

Теперь предположим, что у нас есть приложение, работающее в регионе us-west с локальным часовым поясом UTC-8, и нам нужно записать локальную временную метку 2026-01-01 02:00:00, которая в UTC соответствует 2026-01-01 10:00:00:
  • При записи в виде строки её нужно преобразовать в часовой пояс сервера или столбца.
  • При записи в виде встроенной в язык структуры времени драйвер должен знать целевой часовой пояс, но:
    • Это не всегда возможно
    • API драйвера для этого не слишком хорошо продуман
    • Единственный вариант — описать, какие преобразования будут выполняться, чтобы приложение могло это компенсировать (или записать Unix-временную метку как число)

API временных меток в Java и JDBC

В Java и JDBC есть разные способы задать временную метку:
  1. Использовать класс Timestamp, который по сути является Unix-временной меткой.
    1. При использовании с объектом Calendar это позволяет переинтерпретировать Timestamp в часовом поясе календаря.
    2. У Timestamp есть внутренний календарь, что не совсем очевидно.
  2. Использовать класс LocalDateTime, который легко преобразовать в любой часовой пояс, но при этом нет метода, позволяющего передать целевой часовой пояс.
  3. Использовать класс ZonedDateTime, который помогает преобразовывать часовые пояса при записи в DateTime без часового пояса (поскольку в этом случае известно, что нужно использовать часовой пояс сервера).
    1. Но запись ZonedDateTime в столбец с заданным часовым поясом требует от пользователя компенсировать преобразование, выполняемое драйвером.
  4. Использовать Long для записи миллисекунд Unix-временной метки.
  5. Использовать String, чтобы выполнять все преобразования на стороне приложения (что не очень переносимо).
Предпочтительно использовать java.time.ZoneId#of(java.lang.String) при поиске часового пояса по идентификатору. Этот метод сгенерирует исключение, если часовой пояс не найден (java.util.TimeZone#getTimeZone(java.lang.String) без уведомления вернется к GMT).Правильный способ получить часовой пояс Tokyo:TimeZone.getTimeZone(ZoneId.of("Asia/Tokyo"))

Date

Даты по своей природе не привязаны к часовому поясу. Для хранения дат используются типы Date и Date32. Оба типа используют количество дней с Epoch (1970-01-01). Date использует только положительные значения количества дней, поэтому его диапазон заканчивается 2149-06-06. Date32 поддерживает отрицательные значения количества дней, что позволяет охватывать даты до 1970-01-01, но его диапазон меньше (от 1900-01-01 до 2100-01-01, где 0 — это 1970-01-01). ClickHouse интерпретирует 2026-01-01 как 2026-01-01 в любом часовом поясе, и в определениях столбцов параметр часового пояса отсутствует.

Использование java.time.LocalDate

В Java наиболее подходящий класс для представления значений даты — java.time.LocalDate. Клиент использует этот класс для хранения значений в столбцах Date и Date32 (чтение: LocalDate.ofEpochDay((long)readUnsignedShortLE())). Мы рекомендуем использовать java.time.LocalDate, поскольку этот класс не зависит от преобразований часовых поясов и является частью современного API для работы со временем.

Использование java.sql.Date

LocalDate появился в Java 8. До этого для записи и чтения дат использовался java.sql.Date. Внутри этот класс представляет собой обёртку над моментом времени (значением времени, представляющим абсолютную точку на временной шкале). Поэтому toString() возвращает разную дату в зависимости от часового пояса JVM. Из-за этого драйвер должен тщательно формировать значения, а пользователь — учитывать эту особенность.

Переинтерпретация на основе календаря

У java.sql.ResultSet есть метод для получения значений даты, который принимает Calendar, и аналогичный метод есть у java.sql.PreparedStatement. Это сделано для того, чтобы JDBC-драйвер мог переинтерпретировать значение даты в указанном часовом поясе. Например, в DB хранится значение 2026-01-01, но приложение хочет видеть эту дату как полночь в Tokyo. Это означает, что возвращаемый объект java.sql.Date будет соответствовать конкретному моменту времени, и при преобразовании в локальный часовой пояс это уже может оказаться другой датой из-за разницы во времени. Того же эффекта можно добиться с LocalDate, используя java.time.LocalDate#atStartOfDay(java.time.ZoneId). ClickHouse JDBC-драйвер всегда возвращает объект java.sql.Date, который указывает на локальную дату в полночь. Иными словами, если дата — 2026-01-01, имеется в виду 2026-01-01 12:00 AM в часовом поясе JVM (то же поведение, что и у JDBC-драйверов PostgreSQL и MariaDB).

Время

Значения времени, как и значения Date, в большинстве случаев не привязаны к часовому поясу. ClickHouse не преобразует литералы времени в какой-либо часовой пояс — ’6:30’ везде интерпретируется одинаково.

Типы Time в ClickHouse

Time и Time64 были добавлены в 25.6. До этого вместо них использовались типы временных меток DateTime и DateTime64 (они рассматриваются далее в этом руководстве). Time хранится как 32-битное целое число, представляющее количество секунд, и имеет диапазон [-999:59:59, 999:59:59]. Time64 кодируется как беззнаковый Decimal64 и хранит разные единицы времени в зависимости от точности. Обычно используются значения 3 (миллисекунды), 6 (микросекунды) и 9 (наносекунды). Диапазон значений точности — [0, 9].

Сопоставление типов Java

Клиент считывает значения Time и Time64 и сохраняет их как LocalDateTime. Это сделано для поддержки отрицательного диапазона времени (LocalTime его не поддерживает). В этом случае в качестве даты используется дата эпохи 1970-01-01, поэтому отрицательные значения будут приходиться на время до этой даты. Основная поддержка типов времени реализована с помощью LocalTime (когда значение укладывается в пределы суток) и Duration для использования полного диапазона значений. LocalDateTime можно использовать только для чтения.

Использование java.sql.Time

Использование java.sql.Time ограничено диапазоном значений LocalTime. Внутри java.sql.Time преобразуется в строковый литерал. Значение можно изменить, передав параметр Calendar в PreparedStatement#setTime().

Функция toTime

Временная метка

Временная метка — это определённый момент времени. Например, Unix-временная метка представляет любой момент времени как число секунд относительно 1970-01-01 00:00:00 UTC (отрицательное число секунд обозначает временную метку до эпохи Unix, а положительное — после неё). Такое представление легко вычислять и обрабатывать, если наблюдатель находится в часовом поясе UTC или использует его вместо локального.

Типы временных меток в ClickHouse

В ClickHouse есть типы временных меток DateTime (32-битное целое число, разрешение всегда в секундах) и DateTime64 (64-битное целое число, разрешение зависит от определения). Значения всегда хранятся как временные метки UTC. Это означает, что при представлении в виде чисел преобразование часового пояса не выполняется.

Строковое представление и работа с часовыми поясами

Со строковым представлением связаны некоторые особенности:
  • Если в определении столбца не указан часовой пояс и при записи передаётся строка, она преобразуется из часового пояса сервера в числовую UTC-временную метку. При чтении значения из такого столбца оно преобразуется из UTC-временной метки в буквальную временную метку с использованием часового пояса сервера или сеанса (аналогичный подход применяется к литералам временных меток в выражениях, где часовой пояс явно не задан).
  • Если в определении столбца указан часовой пояс, то во всех строковых преобразованиях используется только он. Это отличается от логики, применяемой, когда часовой пояс не указан, поэтому важно хорошо понимать, как данные записываются в каждый столбец в запросе.
  • Если дата передаётся в виде строки в формате, включающем часовой пояс, требуется функция преобразования. Обычно используется parseDateTimeBestEffort.

Как JDBC-драйвер обрабатывает временные метки

В JDBC-драйвере мы преобразуем временные метки в числовое представление:
"fromUnixTimestamp64Nano(" + epochSeconds * 1_000_000_000L + nanos + ")"
Такое представление решает большинство проблем с преобразованием значений временных меток, поскольку данные отправляются на сервер в едином формате. Однако такой подход требует небольшой корректировки SQL-команд, но при этом остаётся самым простым и понятным способом записывать временные метки в любой столбец. DateTime и DateTime64 считываются и хранятся на клиенте как java.time.ZonedDateTime, что позволяет преобразовывать такие значения в любой другой часовой пояс (информация о часовом поясе при этом сохраняется).

Распространённая ошибка при использовании toDateTime64

Следующий пример кода выглядит корректно, но проверка утверждения не проходит:
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);
    }
}
Это происходит потому, что toDateTime64 использует часовой пояс сервера и не знает о часовом поясе источника данных.

Таблицы преобразований

Если пара преобразования не указана в таблицах ниже, значит, такое преобразование не поддерживается. Например, столбцы Date нельзя читать как java.sql.Timestamp, потому что у них нет части, отвечающей за время. Драйвер не преобразует целочисленные значения ни в какие значения даты/времени. Вызов pstmt.setLong("timestamp", 1772132359L) приведет к тому, что 1772132359 будет записано на сервер как число, которое будет интерпретировано как Unix-временная метка UTC в секундах.

Запись значений с помощью PreparedStatement#setObject

В таблице ниже показано, как преобразуются значения при передаче через PreparedStatement#setObject(column, value):
Класс valueПреобразование
java.time.LocalDateФорматируется как YYYY-MM-DD.
java.sql.DateПреобразуется с использованием календаря по умолчанию и форматируется как LocalDate (YYYY-MM-DD).
java.time.LocalTimeФорматируется как HH:mm:ss.
java.time.DurationФорматируется как HHH:mm:ss. Значение может быть отрицательным.
java.sql.TimeПреобразуется с использованием календаря по умолчанию и форматируется как LocalTime (HH:mm).
java.time.LocalDateTimeПреобразуется в Unix-временную метку в наносекундах и оборачивается вызовом fromUnixTimestamp64Nano.
java.time.ZonedDateTimeПреобразуется в Unix-временную метку в наносекундах и оборачивается вызовом fromUnixTimestamp64Nano.
java.sql.TimestampПреобразуется в Unix-временную метку в наносекундах и оборачивается вызовом fromUnixTimestamp64Nano.
Тип столбца следует считать неизвестным. Приложение само решает, что передавать в подготовленный оператор.

Чтение значений с помощью ResultSet#getObject

В следующей таблице показано, как преобразуются значения при чтении с помощью ResultSet#getObject(column, class):
Тип данных ClickHouse для columnЗначение classПреобразование
Date или Date32java.time.LocalDateЗначение DB (количество дней) преобразуется в LocalDate.
Date или Date32java.sql.DateЗначение DB (количество дней) сначала преобразуется в LocalDate, а затем — в java.sql.Date, где в качестве времени используется полночь в локальном часовом поясе. Если используется календарь, вместо локального часового пояса будет использован его часовой пояс. Пример: значение DB 1970-01-10LocalDate равно 1970-01-10.
Time или Time64java.time.LocalTimeЗначение DB преобразуется в LocalDateTime, а затем в LocalTime. Это работает только для времени в пределах суток.
Time или Time64java.time.LocalDateTimeЗначение DB преобразуется в LocalDateTime.
Time или Time64java.sql.TimeЗначение DB преобразуется в LocalDateTime, а затем в java.sql.Time с использованием календаря по умолчанию. Это работает только для времени в пределах суток.
Time или Time64java.time.DurationЗначение DB преобразуется в LocalDateTime, а затем в Duration.
DateTime или DateTime64java.time.LocalDateTimeЗначение DB преобразуется в ZonedDateTime, а затем в LocalDateTime.
DateTime или DateTime64java.time.ZonedDateTimeЗначение DB преобразуется в ZonedDateTime.
DateTime или DateTime64java.sql.TimestampЗначение DB преобразуется в ZonedDateTime, а затем в java.sql.Timestamp с использованием часового пояса по умолчанию.

Использование методов с календарём

Используйте ResultSet#getTime(column, calendar) и ResultSet#getDate(column, calendar), если значения были сохранены с помощью PreparedStatement#setTime(param, value, calendar) и PreparedStatement#setDate(param, value, calendar) соответственно.
Последнее изменение 10 июня 2026 г.