> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-mintlify-86180b7b.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> Руководство по использованию значений Date/Time в JDBC

# Руководство по значениям Date/Time

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

Этот документ задуман как руководство, помогающее принимать решения: в нём описаны задачи, приведены подробности реализации и объяснены возникающие проблемы.

<div id="timezones">
  ## Часовые пояса
</div>

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

<div id="clickhouse-datetime-string-conversion">
  ### Как ClickHouse преобразует строки DateTime
</div>

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`. В этом нет ничего неправильного, но об этом следует помнить.

<div id="writing-timestamps-across-timezones">
  ### Запись временных меток в разных часовых поясах
</div>

Теперь предположим, что у нас есть приложение, работающее в регионе `us-west` с локальным часовым поясом `UTC-8`, и нам нужно записать локальную временную метку `2026-01-01 02:00:00`, которая в `UTC` соответствует `2026-01-01 10:00:00`:

* При записи в виде строки её нужно преобразовать в часовой пояс сервера или столбца.
* При записи в виде встроенной в язык структуры времени драйвер должен знать целевой часовой пояс, но:
  * Это не всегда возможно
  * API драйвера для этого не слишком хорошо продуман
  * Единственный вариант — описать, какие преобразования будут выполняться, чтобы приложение могло это компенсировать (или записать Unix-временную метку как число)

<div id="java-and-jdbc-timestamp-apis">
  ### API временных меток в Java и JDBC
</div>

В Java и JDBC есть разные способы задать временную метку:

1. Использовать класс `Timestamp`, который по сути является Unix-временной меткой.
   1. При использовании с объектом `Calendar` это позволяет переинтерпретировать `Timestamp` в часовом поясе календаря.
   2. У `Timestamp` есть внутренний календарь, что не совсем очевидно.
2. Использовать класс `LocalDateTime`, который легко преобразовать в любой часовой пояс, но при этом нет метода, позволяющего передать целевой часовой пояс.
3. Использовать класс `ZonedDateTime`, который помогает преобразовывать часовые пояса при записи в `DateTime` без часового пояса (поскольку в этом случае известно, что нужно использовать часовой пояс сервера).
   1. Но запись `ZonedDateTime` в столбец с заданным часовым поясом требует от пользователя компенсировать преобразование, выполняемое драйвером.
4. Использовать `Long` для записи миллисекунд Unix-временной метки.
5. Использовать `String`, чтобы выполнять все преобразования на стороне приложения (что не очень переносимо).

<Warning>
  Предпочтительно использовать `java.time.ZoneId#of(java.lang.String)` при поиске часового пояса по идентификатору.
  Этот метод сгенерирует исключение, если часовой пояс не найден (`java.util.TimeZone#getTimeZone(java.lang.String)` без уведомления вернется к `GMT`).

  Правильный способ получить часовой пояс `Tokyo`:

  `TimeZone.getTimeZone(ZoneId.of("Asia/Tokyo"))`
</Warning>

<div id="date">
  ## Date
</div>

Даты по своей природе не привязаны к часовому поясу. Для хранения дат используются типы `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` в любом часовом поясе, и в определениях столбцов параметр часового пояса отсутствует.

<div id="using-localdate">
  ### Использование `java.time.LocalDate`
</div>

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

Мы рекомендуем использовать `java.time.LocalDate`, поскольку этот класс не зависит от преобразований часовых поясов и является частью современного API для работы со временем.

<div id="using-java-sql-date">
  ### Использование `java.sql.Date`
</div>

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

<div id="calendar-based-reinterpretation">
  ### Переинтерпретация на основе календаря
</div>

У `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).

<div id="time">
  ## Время
</div>

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

<div id="clickhouse-time-types">
  ### Типы Time в ClickHouse
</div>

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

<div id="java-type-mapping">
  ### Сопоставление типов Java
</div>

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

Основная поддержка типов времени реализована с помощью `LocalTime` (когда значение укладывается в пределы суток) и `Duration` для использования полного диапазона значений. `LocalDateTime` можно использовать только для чтения.

<div id="using-java-sql-time">
  ### Использование `java.sql.Time`
</div>

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

<div id="totime-function">
  ### Функция `toTime`
</div>

<Note>
  * `toTime` всегда требует `Date`, `DateTime` или другой аналогичный тип. Строки не принимаются. Связанная проблема: [https://github.com/ClickHouse/ClickHouse/issues/89896](https://github.com/ClickHouse/ClickHouse/issues/89896)
  * Имеет псевдоним [`toTimeWithFixedDate`](/ru/reference/functions/regular-functions/date-time-functions#toTimeWithFixedDate).
  * Есть проблема, связанная с часовыми поясами: [https://github.com/ClickHouse/ClickHouse/pull/90310](https://github.com/ClickHouse/ClickHouse/pull/90310)
</Note>

<div id="timestamp">
  ## Временная метка
</div>

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

<div id="clickhouse-timestamp-types">
  ### Типы временных меток в ClickHouse
</div>

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

<div id="string-representation-and-timezone-behavior">
  ### Строковое представление и работа с часовыми поясами
</div>

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

* Если в определении столбца не указан часовой пояс и при записи передаётся строка, она преобразуется из часового пояса сервера в числовую UTC-временную метку. При чтении значения из такого столбца оно преобразуется из UTC-временной метки в буквальную временную метку с использованием часового пояса сервера или сеанса (аналогичный подход применяется к литералам временных меток в выражениях, где часовой пояс явно не задан).
* Если в определении столбца указан часовой пояс, то во всех строковых преобразованиях используется только он. Это отличается от логики, применяемой, когда часовой пояс не указан, поэтому важно хорошо понимать, как данные записываются в каждый столбец в запросе.
* Если дата передаётся в виде строки в формате, включающем часовой пояс, требуется функция преобразования. Обычно используется [`parseDateTimeBestEffort`](/ru/reference/functions/regular-functions/type-conversion-functions#parseDateTimeBestEffort).

<div id="how-jdbc-driver-handles-timestamps">
  ### Как JDBC-драйвер обрабатывает временные метки
</div>

В JDBC-драйвере мы преобразуем временные метки в числовое представление:

```java theme={null}
"fromUnixTimestamp64Nano(" + epochSeconds * 1_000_000_000L + nanos + ")"
```

Такое представление решает большинство проблем с преобразованием значений временных меток, поскольку данные отправляются на сервер в едином формате. Однако такой подход требует небольшой корректировки SQL-команд, но при этом остаётся самым простым и понятным способом записывать временные метки в любой столбец.

`DateTime` и `DateTime64` считываются и хранятся на клиенте как `java.time.ZonedDateTime`, что позволяет преобразовывать такие значения в любой другой часовой пояс (информация о часовом поясе при этом сохраняется).

<div id="common-pitfall-todatetime64">
  ### Распространённая ошибка при использовании `toDateTime64`
</div>

Следующий пример кода выглядит корректно, но проверка утверждения не проходит:

```java theme={null}
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` использует часовой пояс сервера и не знает о часовом поясе источника данных.

<div id="conversion-tables">
  ## Таблицы преобразований
</div>

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

<div id="writing-values-setobject">
  ### Запись значений с помощью `PreparedStatement#setObject`
</div>

В таблице ниже показано, как преобразуются значения при передаче через `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`. |

<Note>
  Тип столбца следует считать неизвестным. Приложение само решает, что передавать в подготовленный оператор.
</Note>

<div id="reading-values-getobject">
  ### Чтение значений с помощью `ResultSet#getObject`
</div>

В следующей таблице показано, как преобразуются значения при чтении с помощью `ResultSet#getObject(column, class)`:

| Тип данных ClickHouse для `column` | Значение `class`          | Преобразование                                                                                                                                                                                                                                                                                                                              |
| ---------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Date` или `Date32`                | `java.time.LocalDate`     | Значение DB (количество дней) преобразуется в `LocalDate`.                                                                                                                                                                                                                                                                                  |
| `Date` или `Date32`                | `java.sql.Date`           | Значение DB (количество дней) сначала преобразуется в `LocalDate`, а затем — в `java.sql.Date`, где в качестве времени используется полночь в локальном часовом поясе. Если используется календарь, вместо локального часового пояса будет использован его часовой пояс. Пример: значение DB `1970-01-10` → `LocalDate` равно `1970-01-10`. |
| `Time` или `Time64`                | `java.time.LocalTime`     | Значение DB преобразуется в `LocalDateTime`, а затем в `LocalTime`. Это работает только для времени в пределах суток.                                                                                                                                                                                                                       |
| `Time` или `Time64`                | `java.time.LocalDateTime` | Значение DB преобразуется в `LocalDateTime`.                                                                                                                                                                                                                                                                                                |
| `Time` или `Time64`                | `java.sql.Time`           | Значение DB преобразуется в `LocalDateTime`, а затем в `java.sql.Time` с использованием календаря по умолчанию. Это работает только для времени в пределах суток.                                                                                                                                                                           |
| `Time` или `Time64`                | `java.time.Duration`      | Значение DB преобразуется в `LocalDateTime`, а затем в `Duration`.                                                                                                                                                                                                                                                                          |
| `DateTime` или `DateTime64`        | `java.time.LocalDateTime` | Значение DB преобразуется в `ZonedDateTime`, а затем в `LocalDateTime`.                                                                                                                                                                                                                                                                     |
| `DateTime` или `DateTime64`        | `java.time.ZonedDateTime` | Значение DB преобразуется в `ZonedDateTime`.                                                                                                                                                                                                                                                                                                |
| `DateTime` или `DateTime64`        | `java.sql.Timestamp`      | Значение DB преобразуется в `ZonedDateTime`, а затем в `java.sql.Timestamp` с использованием часового пояса по умолчанию.                                                                                                                                                                                                                   |

<div id="using-calendar-based-methods">
  ### Использование методов с календарём
</div>

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