sqlpostgresqldate-partextract

SQL DATE_PART: Numeric Date Fields, Day of Week, and Dynamic Field by String

How DATE_PART returns a numeric date field, how it differs from EXTRACT, and why dow numbers Sunday as 0.

3 min branjaReferencesql · postgresql · date-part · extract · date-functions · mysql
Ta članek je trenutno v ruščini — angleški prevod je v pripravi.

DATE_PART вытаскивает из метки времени одну числовую часть: час, день недели, номер недели, секунды от эпохи. Это функциональная запись того же, что делает оператор EXTRACT, но поле здесь — обычная строка, а значит, его можно подставлять динамически.

DATE_PART пригождается там, где из колонки timestamp нужно вытащить не саму дату, а одно её поле как число: час события для почасовой витрины, день недели для отчёта по дням, номер ISO-недели для группировки, секунды от эпохи для расчёта возраста или длительности. Поскольку имя поля передаётся строкой, его удобно подставлять параметром, а результат double precision сразу годится для GROUP BY, AVG и арифметики. Главные тонкости касаются типа результата, нумерации дней недели ('dow' против 'isodow') и поведения 'epoch' на интервалах — их и разбираем ниже.

Что возвращает DATE_PART

Первый аргумент — имя поля строкой ('hour', 'dow', 'doy', 'week', 'month', 'epoch' и т. д.), второй — timestamp или date. Результат всегда double precision, то есть число, а не интервал или строка.

SELECT
  DATE_PART('hour',  TIMESTAMP '2026-06-17 14:32:09') AS hour,
  DATE_PART('dow',   TIMESTAMP '2026-06-17 14:32:09') AS day_of_week,
  DATE_PART('doy',   TIMESTAMP '2026-06-17 14:32:09') AS day_of_year,
  DATE_PART('week',  TIMESTAMP '2026-06-17 14:32:09') AS iso_week;
-- 14 | 3 | 168 | 25

Полезные поля:

  • 'hour', 'minute', 'second' — компоненты времени; 'second' включает дробную часть.
  • 'year', 'month', 'day' — компоненты даты.
  • 'dow' — день недели, 0 = воскресенье ... 6 = суббота.
  • 'doy' — день года от 1 до 366.
  • 'week' — номер недели по ISO 8601.
  • 'epoch' — число секунд с 1970-01-01 00:00:00 UTC.

DATE_PART против EXTRACT

EXTRACT('hour' FROM ts) и DATE_PART('hour', ts) дают одно и то же число. Разница — синтаксическая: EXTRACT это оператор со словом FROM, а DATE_PART обычная функция.

-- These two lines are equivalent
SELECT EXTRACT(dow FROM created_at) FROM orders;
SELECT DATE_PART('dow', created_at) FROM orders;

Практический плюс функции в том, что поле — строковый литерал, и его легко передать как параметр. Можно сгруппировать заказы по любой части даты, не переписывая запрос:

SELECT
  DATE_PART('hour', created_at) AS hour_of_day,
  COUNT(*)                      AS orders
FROM orders
WHERE status = 'paid'
GROUP BY DATE_PART('hour', created_at)
ORDER BY hour_of_day;

В PostgreSQL 14+ EXTRACT возвращает numeric, а DATE_PART по-прежнему double precision; для сравнений и ROUND это иногда важно.

День недели: dow против isodow

Главная путаница — нумерация дней. 'dow' начинает неделю с воскресенья и нумерует его нулём; 'isodow' следует ISO, где понедельник 1, а воскресенье 7.

SELECT
  DATE_PART('dow',    DATE '2026-06-21') AS dow,     -- Sunday -> 0
  DATE_PART('isodow', DATE '2026-06-21') AS isodow;  -- Sunday -> 7

Типичный отчёт «активность по дням недели» с человекочитаемой меткой:

SELECT
  DATE_PART('isodow', created_at) AS weekday_num,
  TO_CHAR(created_at, 'Dy')       AS weekday,
  COUNT(*)                        AS signups
FROM users
GROUP BY DATE_PART('isodow', created_at), TO_CHAR(created_at, 'Dy')
ORDER BY weekday_num;

Ловушка: если фильтровать выходные через DATE_PART('dow', d) IN (0, 6), а потом случайно перейти на isodow, суббота и воскресенье станут 6 и 7 — условие IN (0, 6) молча начнёт пропускать воскресенье. Всегда фиксируйте, какое именно поле используете.

Эпоха и разница во времени

Поле 'epoch' превращает метку в секунды, а из интервала достаёт его полную длительность в секундах. Это удобный способ посчитать возраст или длительность одним числом.

SELECT
  id,
  DATE_PART('epoch', NOW() - created_at) / 86400 AS account_age_days
FROM users
ORDER BY account_age_days DESC
LIMIT 10;

Та же идея для бизнес-метрики «средний возраст заказа в часах» по отделам сотрудников:

SELECT
  e.dept,
  AVG(DATE_PART('epoch', NOW() - o.created_at) / 3600) AS avg_order_age_hours
FROM orders o
JOIN users u    ON u.id = o.user_id
JOIN employees e ON e.name = u.name
GROUP BY e.dept;

Помните: DATE_PART('epoch', interval) для интервала длиннее суток вернёт полные секунды, а не «часть дня», в отличие от 'hour', который у интервала даёт только часовую компоненту.

Различия в MySQL и ClickHouse

Имя DATE_PART не универсально.

  • MySQL функции DATE_PART нет. Используйте EXTRACT(HOUR FROM ts) или отдельные функции HOUR(), DAYOFWEEK() (где воскресенье 1), DAYOFYEAR(), WEEK().
SELECT
  HOUR(created_at)      AS hour_of_day,
  DAYOFWEEK(created_at) AS dow_sun_is_1,
  COUNT(*)              AS orders
FROM orders
GROUP BY HOUR(created_at), DAYOFWEEK(created_at);
  • ClickHouse предлагает toHour, toDayOfWeek (понедельник 1), toDayOfYear, toUnixTimestamp для эпохи.
SELECT
  toHour(created_at)      AS hour_of_day,
  toDayOfWeek(created_at) AS dow_mon_is_1,
  count()                 AS orders
FROM orders
GROUP BY hour_of_day, dow_mon_is_1;

Главная засада при переносе — именно нумерация дней недели: DATE_PART('dow', d) в PostgreSQL даёт 0 для воскресенья, DAYOFWEEK() в MySQL для того же воскресенья даёт 1, а toDayOfWeek в ClickHouse начинает неделю с понедельника (1). Любой фильтр или CASE, завязанный на конкретные числа дней, после смены СУБД нужно пересчитывать заново. Вторая тонкость — тип результата: DATE_PART всегда возвращает double precision, тогда как HOUR() и подобные функции MySQL отдают целое, а EXTRACT в PostgreSQL 14+ — numeric, и при сравнении дробных секунд это иногда расходится.

Отдельно стоит помнить про индексы: условие вида DATE_PART('year', created_at) = 2026 оборачивает колонку в функцию, поэтому обычный B-tree-индекс по created_at не используется и запрос идёт по полному скану. Если фильтр горячий, либо переписывайте его как диапазон created_at >= DATE '2026-01-01' AND created_at < DATE '2027-01-01', либо заводите выражательный индекс прямо по DATE_PART('year', created_at). Для группировок в отчётах это не критично, но в WHERE по большой таблице разница в плане выполнения видна сразу.

Суть везде одна — достать число из даты, но нумерация дней недели у каждой СУБД своя, и это первое, что стоит проверить при переносе запроса.

Vadite na resničnih nalogah

Rešujte naloge v vadnici SQL s takojšnjim ocenjevanjem in namigi.

Odpri vadnico