Часовые пояса
Как ClickHouse преобразует строки DateTime
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
- Использовать класс
Timestamp, который по сути является Unix-временной меткой.- При использовании с объектом
Calendarэто позволяет переинтерпретироватьTimestampв часовом поясе календаря. - У
Timestampесть внутренний календарь, что не совсем очевидно.
- При использовании с объектом
- Использовать класс
LocalDateTime, который легко преобразовать в любой часовой пояс, но при этом нет метода, позволяющего передать целевой часовой пояс. - Использовать класс
ZonedDateTime, который помогает преобразовывать часовые пояса при записи вDateTimeбез часового пояса (поскольку в этом случае известно, что нужно использовать часовой пояс сервера).- Но запись
ZonedDateTimeв столбец с заданным часовым поясом требует от пользователя компенсировать преобразование, выполняемое драйвером.
- Но запись
- Использовать
Longдля записи миллисекунд Unix-временной метки. - Использовать
String, чтобы выполнять все преобразования на стороне приложения (что не очень переносимо).
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.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).
Время
’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
toTimeвсегда требуетDate,DateTimeили другой аналогичный тип. Строки не принимаются. Связанная проблема: https://github.com/ClickHouse/ClickHouse/issues/89896- Имеет псевдоним
toTimeWithFixedDate. - Есть проблема, связанная с часовыми поясами: https://github.com/ClickHouse/ClickHouse/pull/90310
Временная метка
1970-01-01 00:00:00 UTC (отрицательное число секунд обозначает временную метку до эпохи Unix, а положительное — после неё). Такое представление легко вычислять и обрабатывать, если наблюдатель находится в часовом поясе UTC или использует его вместо локального.
Типы временных меток в ClickHouse
DateTime (32-битное целое число, разрешение всегда в секундах) и DateTime64 (64-битное целое число, разрешение зависит от определения). Значения всегда хранятся как временные метки UTC. Это означает, что при представлении в виде чисел преобразование часового пояса не выполняется.
Строковое представление и работа с часовыми поясами
- Если в определении столбца не указан часовой пояс и при записи передаётся строка, она преобразуется из часового пояса сервера в числовую UTC-временную метку. При чтении значения из такого столбца оно преобразуется из UTC-временной метки в буквальную временную метку с использованием часового пояса сервера или сеанса (аналогичный подход применяется к литералам временных меток в выражениях, где часовой пояс явно не задан).
- Если в определении столбца указан часовой пояс, то во всех строковых преобразованиях используется только он. Это отличается от логики, применяемой, когда часовой пояс не указан, поэтому важно хорошо понимать, как данные записываются в каждый столбец в запросе.
- Если дата передаётся в виде строки в формате, включающем часовой пояс, требуется функция преобразования. Обычно используется
parseDateTimeBestEffort.
Как JDBC-драйвер обрабатывает временные метки
DateTime и DateTime64 считываются и хранятся на клиенте как java.time.ZonedDateTime, что позволяет преобразовывать такие значения в любой другой часовой пояс (информация о часовом поясе при этом сохраняется).
Распространённая ошибка при использовании toDateTime64
toDateTime64 использует часовой пояс сервера и не знает о часовом поясе источника данных.
Таблицы преобразований
Date нельзя читать как java.sql.Timestamp, потому что у них нет части, отвечающей за время.
Драйвер не преобразует целочисленные значения ни в какие значения даты/времени. Вызов pstmt.setLong("timestamp", 1772132359L) приведет к тому, что 1772132359 будет записано на сервер как число, которое будет интерпретировано как
Unix-временная метка UTC в секундах.
Запись значений с помощью PreparedStatement#setObject
PreparedStatement#setObject(column, value):
Тип столбца следует считать неизвестным. Приложение само решает, что передавать в подготовленный оператор.
Чтение значений с помощью ResultSet#getObject
ResultSet#getObject(column, class):
Использование методов с календарём
ResultSet#getTime(column, calendar) и ResultSet#getDate(column, calendar), если значения были сохранены с помощью PreparedStatement#setTime(param, value, calendar) и PreparedStatement#setDate(param, value, calendar) соответственно.