Learning Platform
Глоссарий Troubleshooting
Урок 05.02 · 23 мин
Средний
type-systemdatetimetimestamp

Дата и время: DATE, TIME, TIMESTAMP, INTERVAL

Время — самый коварный тип данных в любой СУБД. Не потому, что оно сложно устроено внутри (как раз внутри всё просто — целое число), а потому, что вокруг него много неявной семантики: часовые пояса, переход на летнее время, високосные годы, разница между «моментом во вселенной» и «показанием настенных часов». Аналитика временных рядов — одна из главных задач DuckDB, поэтому понимать его типы даты и времени нужно точно.

В этом уроке разберём четыре основных типа — DATE, TIME, TIMESTAMP, INTERVAL — плюс критически важный TIMESTAMPTZ. Главная мысль: внутри все они хранятся как обыкновенные целые числа-смещения, а вся «сложность времени» — это правила интерпретации этих целых. И самая частая ошибка — путаница между TIMESTAMP и TIMESTAMPTZ.


Внутри — целое число-смещение

DuckDB не хранит дату как текст «2026-01-15» и не хранит структуру с полями год/месяц/день. Каждый временной тип внутри — это одно целое число, отсчитанное от фиксированной точки отсчёта (epoch).

  • DATE — целое число дней от 1 января 1970 года. 1970-01-01 это 0, 1970-01-02 это 1, 2026-01-15 это какое-то положительное число дней.
  • TIMESTAMP — целое число микросекунд от той же точки 1970-01-01 00:00:00.
  • TIME — целое число микросекунд от начала суток (от 00:00:00).
-- DATE как число дней от epoch, TIMESTAMP как микросекунды
SELECT
    epoch_days(DATE '2026-01-15')        AS days_since_1970,
    epoch_us(TIMESTAMP '2026-01-15 12:00:00') AS us_since_1970;
Временной тип — это целочисленное смещение от epoch
DATE '2026-01-15'Человекочитаемая запись даты. Это лишь представление при выводе.
хранится как
целое число днейВнутри DuckDB DATE — это 4-байтное целое: количество дней, прошедших с 1 января 1970 года.
сравнение / арифметика
операции над целымиСравнение дат, разность, сдвиг — это сравнение и арифметика обычных целых чисел. Быстро и без неоднозначностей.

Почему это устроено именно так — три причины. Первая: целое число занимает мало места и фиксированную ширину, колонка дат — это плотный массив целых. Вторая: сравнение и сортировка дат превращаются в сравнение целых — самую быструю операцию процессора; «дата A раньше даты B» это «целое A меньше целого B». Третья: смещение от общей точки отсчёта — идеальный вход для сжатия Frame of Reference, которое хранит близкие даты как маленькие отклонения от минимума (это разбирается в модуле про компрессию). Человекочитаемый вид «2026-01-15» собирается из целого только в момент вывода.


DATE, TIME и TIMESTAMP

DATE хранит календарную дату без времени суток — год, месяц, день. Используется, когда время дня не имеет смысла: дата рождения, дата отчётного периода, день сделки.

TIME хранит время суток без даты — часы, минуты, секунды, микросекунды. Используется редко само по себе: «время открытия магазина», расписание. Чаще время — часть TIMESTAMP.

TIMESTAMP хранит момент: дату и время суток вместе, с микросекундной точностью. Это основной тип для событийных данных — лог, транзакция, измерение сенсора. Большинство временных колонок в аналитике — TIMESTAMP.

-- Литералы трёх типов
SELECT
    DATE '2026-01-15'                  AS d,
    TIME '14:30:00'                    AS t,
    TIMESTAMP '2026-01-15 14:30:00'    AS ts;

-- Извлечение компонентов из TIMESTAMP
SELECT
    extract(year  FROM TIMESTAMP '2026-01-15 14:30:00') AS yr,
    extract(month FROM TIMESTAMP '2026-01-15 14:30:00') AS mon,
    extract(hour  FROM TIMESTAMP '2026-01-15 14:30:00') AS hr;
-- yr=2026, mon=1, hr=14

DuckDB поддерживает и варианты TIMESTAMP с другой точностью — например, секундную или наносекундную, — но точность по умолчанию микросекундная, и для подавляющего большинства задач её достаточно. Наносекунды нужны разве что в высокочастотной торговле или приборном телеметрировании.


TIMESTAMP против TIMESTAMPTZ: главная развилка

Вот ключевой момент урока. TIMESTAMP и TIMESTAMPTZ (полное имя TIMESTAMP WITH TIME ZONE) выглядят похоже, но представляют разные сущности.

TIMESTAMP (без часового пояса) — это «показание настенных часов». Просто запись «2026-01-15 14:30:00» без привязки к тому, в каком поясе эти часы висят. Два TIMESTAMP с одинаковыми цифрами равны, даже если один записан в Москве, а другой в Нью-Йорке. TIMESTAMP не знает про пояса вообще.

TIMESTAMPTZ — это «момент во вселенной», абсолютная точка на оси времени. Внутри он хранится так же, целым числом микросекунд от epoch, но это микросекунды UTC. Важно понять: TIMESTAMPTZ не хранит «свой» часовой пояс внутри значения. Он хранит абсолютный момент в UTC, а часовой пояс применяется только при вводе (чтобы перевести введённое локальное время в UTC) и при выводе (чтобы показать UTC-момент в нужном поясе).

TIMESTAMP против TIMESTAMPTZ
TIMESTAMP: показание настенных часовПросто запись даты и времени без привязки к поясу. '14:30' — это 14:30 неважно где. Не участвует в пересчёте поясов.
vs
TIMESTAMPTZ: момент во вселеннойАбсолютная точка на оси времени. Хранится как микросекунды UTC. Пояс применяется при вводе и выводе, но внутри значения не лежит.

Разница видна на практике. TIMESTAMPTZ участвует в пересчёте поясов, TIMESTAMP — нет:

-- Текущая зона сессии влияет на ввод и вывод TIMESTAMPTZ
SET TimeZone = 'UTC';
SELECT TIMESTAMPTZ '2026-01-15 14:30:00' AS t_utc;
-- интерпретируется как 14:30 UTC

SET TimeZone = 'America/New_York';
SELECT TIMESTAMPTZ '2026-01-15 14:30:00+00' AS t_ny;
-- тот же UTC-момент, но показан в нью-йоркском времени: 09:30

Один и тот же абсолютный момент TIMESTAMPTZ показывается разными цифрами в разных поясах — потому что это один момент, на который смотрят с разных «настенных часов». TIMESTAMP так не делает: его цифры неизменны.

СвойствоTIMESTAMPTIMESTAMPTZ
Что представляетНастенные часы, локальное времяАбсолютный момент на оси времени
Хранит пояс внутриНетНет (хранит UTC-момент)
Зависит от зоны сессииНетДа — при вводе и выводе
Когда применятьДата без географии: «отчётный день»Событие, которое произошло: лог, транзакция

Практическое правило: для любого события, которое реально произошло в определённый момент (запись в логе, оплата, клик), используйте TIMESTAMPTZ — вам важен абсолютный момент, и пользователи из разных поясов должны видеть его корректно в своём времени. TIMESTAMP без пояса оставьте для случаев, где география несущественна: «время начала рабочего дня» как абстрактное 09:00, плановое расписание.

DANGER

Самая частая ошибка с временем — смешивать TIMESTAMP и TIMESTAMPTZ в одной системе, особенно хранить реальные события в TIMESTAMP без пояса. Тогда «когда произошло событие» становится неоднозначным: 14:30 в чьём поясе? При сравнении событий из разных источников или при показе пользователям в разных странах это приводит к ошибкам в несколько часов. Решите на старте: события — TIMESTAMPTZ, и придерживайтесь этого.


INTERVAL: длительность, а не момент

INTERVAL — отдельная категория. Все типы выше представляют точку на оси времени. INTERVAL представляет длительность — отрезок: «3 дня», «2 часа 30 минут», «1 год».

Устроен INTERVAL любопытно: внутри это не одно число, а три независимых поля — месяцы, дни и микросекунды. Почему три, а не пересчёт всего в микросекунды? Потому что «месяц» и «день» — не фиксированные величины. В месяце бывает 28, 29, 30 или 31 день; в сутках при переходе на летнее время бывает 23 или 25 часов. INTERVAL '1 month' нельзя заранее перевести в число дней — это зависит от того, к какой дате его прибавляют. Поэтому DuckDB хранит компоненты раздельно и пересчитывает их только в момент сложения с конкретной датой.

-- INTERVAL — результат вычитания двух дат или явный литерал
SELECT DATE '2026-03-01' - DATE '2026-01-15' AS gap;
-- результат: INTERVAL 45 days

-- Арифметика момент +/- длительность
SELECT TIMESTAMP '2026-01-15 10:00:00' + INTERVAL 3 HOUR AS later;
-- результат: 2026-01-15 13:00:00

-- '1 month' прибавляется по-разному к разным датам
SELECT
    DATE '2026-01-31' + INTERVAL 1 MONTH AS from_jan31,
    DATE '2026-02-15' + INTERVAL 1 MONTH AS from_feb15;
-- from_jan31 = 2026-02-28 (февраль короткий!), from_feb15 = 2026-03-15

Последний пример показывает, почему INTERVAL хранит месяцы отдельно: + 1 MONTH к 31 января даёт 28 февраля (последний день февраля), а к 15 февраля — 15 марта. Один и тот же INTERVAL, разный результат в днях — потому что пересчёт происходит относительно конкретной даты.

Арифметика времени складывается из трёх паттернов: момент - момент = длительность (INTERVAL), момент + длительность = момент, момент - длительность = момент. Складывать два момента (TIMESTAMP + TIMESTAMP) бессмысленно и запрещено — как нельзя сложить «3 часа дня» с «5 часами вечера».

TIP

DuckDB заимствовал у Postgres удобную функцию date_trunc: она «обрезает» момент до начала периода. date_trunc('month', ts) даёт первое число месяца, date_trunc('day', ts) — полночь. Это рабочая лошадка для группировки временных рядов: чтобы посчитать выручку по месяцам, группируйте по date_trunc('month', order_ts).


Попробуй сам

Запустите DuckDB CLI:

-- Подготовка
SELECT DATE '2026-01-15' AS d, TIMESTAMP '2026-01-15 14:30:00' AS ts;

Задания:

  1. Посчитайте, сколько дней прошло между DATE '2026-01-01' и DATE '2026-12-31', вычитанием дат. Какого типа результат?
  2. Установите SET TimeZone = 'UTC', выведите TIMESTAMPTZ '2026-06-01 12:00:00'. Затем установите SET TimeZone = 'Asia/Tokyo' и выведите тот же литерал с суффиксом +00. Объясните разницу в цифрах.
  3. Выведите TIMESTAMP '2026-06-01 12:00:00' (без TZ) при двух разных зонах сессии. Убедитесь, что цифры НЕ меняются, и объясните почему.
  4. Прибавьте INTERVAL 1 MONTH к DATE '2026-01-31' и к DATE '2026-03-31'. Сравните результаты и объясните, почему INTERVAL хранит месяцы отдельным полем.
  5. Через date_trunc('month', ...) сгруппируйте набор timestamp-ов по месяцам.
Дата и время в PostgreSQL: TIMESTAMP, TIMESTAMPTZ, INTERVAL
Проверка знанийKnowledge check
Как DuckDB хранит DATE и TIMESTAMP на уровне байтов, и в чём принципиальная разница между TIMESTAMP и TIMESTAMPTZ?
ОтветAnswer
Внутри DuckDB временные типы — это обычные целые числа-смещения от фиксированной точки отсчёта (epoch, 1 января 1970 года). DATE — это целое число дней от epoch, TIMESTAMP — целое число микросекунд от epoch, TIME — микросекунды от начала суток. Такое представление компактно, делает сравнение и сортировку дат сравнением целых (самой быстрой операцией процессора) и идеально ложится на сжатие Frame of Reference. Человекочитаемый вид собирается из целого только при выводе. Принципиальная разница TIMESTAMP и TIMESTAMPTZ: TIMESTAMP (без пояса) — это 'показание настенных часов', запись даты и времени без привязки к географии; два TIMESTAMP с одинаковыми цифрами равны, и он не участвует в пересчёте поясов. TIMESTAMPTZ — это 'абсолютный момент на оси времени'; внутри он хранит микросекунды UTC и НЕ хранит свой пояс внутри значения — пояс применяется только при вводе (перевод введённого локального времени в UTC) и при выводе (показ UTC-момента в нужном поясе). Поэтому один TIMESTAMPTZ показывается разными цифрами в разных зонах сессии — это один момент, на который смотрят с разных часов. Практическое правило: реальные события (логи, транзакции, клики) хранят в TIMESTAMPTZ, потому что важен абсолютный момент; TIMESTAMP без пояса оставляют для случаев, где география несущественна. Смешивать их, особенно хранить события в TIMESTAMP без пояса, — частая ошибка, дающая расхождения в часы.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Как DuckDB хранит значение типа DATE на уровне байтов?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6