sqlpostgresqldatetimetimestamp

CURRENT_TIMESTAMP в PostgreSQL: текущее время, которое не всегда «текущее»

CURRENT_TIMESTAMP возвращает timestamptz и фиксируется на старте транзакции; разбираем отличие от LOCALTIMESTAMP и clock_timestamp для аудита и DEFAULT.

10 мин чтенияСправочникsql · postgresql · datetime · timestamp · audit

CURRENT_TIMESTAMP — это стандартная SQL-функция для получения текущей даты и времени.

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

Но в PostgreSQL у этой функции есть важная особенность:

CURRENT_TIMESTAMP возвращает время старта текущей транзакции, а не постоянно тикающее «прямо сейчас».

Именно из-за этого начинающие часто удивляются: между двумя запросами прошло несколько секунд, а время осталось одинаковым.

Разберём спокойно:

  • какой тип возвращает CURRENT_TIMESTAMP;
  • чем он отличается от LOCALTIMESTAMP;
  • почему время «заморожено» внутри транзакции;
  • когда нужен clock_timestamp();
  • как правильно заполнять created_at и updated_at;
  • почему в MySQL и ClickHouse поведение может отличаться.

Базовый пример

Самый простой вызов:

SELECT CURRENT_TIMESTAMP;

В PostgreSQL результат будет иметь тип timestamp with time zone, его часто сокращают до timestamptz.

Пример результата:

2026-01-15 12:34:56.789123+03

Здесь есть дата, время, дробные секунды и часовой пояс вывода.

Важно понимать: timestamptz хранит не «время с приклеенной строкой часового пояса», а абсолютный момент времени. PostgreSQL хранит момент, а показывает его с учётом текущей настройки часового пояса сессии.

Для бизнес-приложений это почти всегда то, что нужно.

Если пользователь из Москвы и пользователь из Лиссабона видят один и тот же заказ, в базе должен храниться один абсолютный момент создания заказа, а не две разные локальные даты.

CURRENT_TIMESTAMP и LOCALTIMESTAMP

В PostgreSQL рядом с CURRENT_TIMESTAMP есть похожая функция LOCALTIMESTAMP.

Сравним их:

SELECT
    CURRENT_TIMESTAMP AS with_zone,
    LOCALTIMESTAMP AS without_zone;

Разница в типах:

Выражение Тип Что означает
CURRENT_TIMESTAMP timestamp with time zone абсолютный момент времени
LOCALTIMESTAMP timestamp without time zone локальные «часы на стене» без привязки к зоне

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

LOCALTIMESTAMP подходит реже. Это просто дата и время без часового пояса. Такое значение можно использовать, если вы точно работаете с локальным расписанием, где важны именно «часы на стене»: например, магазин открывается в 09:00 по местному времени.

Для колонок вроде created_at, updated_at, paid_at, deleted_at в большинстве приложений лучше выбирать timestamptz.

created_at через DEFAULT

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

Например, таблица пользователей:

CREATE TABLE users (
    id         bigserial PRIMARY KEY,
    email      text NOT NULL UNIQUE,
    name       text,
    country    text,
    created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Теперь при вставке пользователя можно не передавать created_at вручную:

INSERT INTO users (email, name, country)
VALUES ('ana@example.com', 'Ana', 'ES');

PostgreSQL сам подставит значение CURRENT_TIMESTAMP.

Это удобно и безопасно: приложение не обязано каждый раз помнить, что нужно отправить дату создания. База сама ставит отметку времени.

Точность CURRENT_TIMESTAMP

У CURRENT_TIMESTAMP можно указать точность дробных секунд.

Например:

SELECT
    CURRENT_TIMESTAMP AS full_precision,
    CURRENT_TIMESTAMP(0) AS seconds_only,
    CURRENT_TIMESTAMP(3) AS milliseconds;

Что это значит:

CURRENT_TIMESTAMP без аргумента вернёт значение с полной доступной точностью.

CURRENT_TIMESTAMP(0) уберёт дробные секунды.

CURRENT_TIMESTAMP(3) оставит миллисекунды.

Примерно так:

Выражение Пример
CURRENT_TIMESTAMP 2026-01-15 12:34:56.789123+03
CURRENT_TIMESTAMP(0) 2026-01-15 12:34:57+03
CURRENT_TIMESTAMP(3) 2026-01-15 12:34:56.789+03

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

Главное: CURRENT_TIMESTAMP — это время транзакции

Теперь самый важный момент.

В PostgreSQL CURRENT_TIMESTAMP показывает время старта текущей транзакции.

То есть внутри одной транзакции значение не меняется.

Пример:

BEGIN;

SELECT CURRENT_TIMESTAMP;

SELECT pg_sleep(3);

SELECT CURRENT_TIMESTAMP;

COMMIT;

Между двумя вызовами прошли три секунды, но оба SELECT CURRENT_TIMESTAMP вернут одно и то же значение.

На первый взгляд это странно. Мы же просим текущее время. Почему оно не изменилось?

Потому что PostgreSQL отвечает не на вопрос «сколько времени на часах прямо сейчас?», а на вопрос:

в какой момент началась текущая транзакция?

И это поведение очень полезно для консистентности.

Почему замороженное время — это удобно

Представьте, что вы создаёте заказ и сразу пишете событие в аудит.

BEGIN;

INSERT INTO orders (user_id, amount, status, created_at)
VALUES (42, 99.90, 'paid', CURRENT_TIMESTAMP);

INSERT INTO order_audit (order_id, event, at)
VALUES (currval('orders_id_seq'), 'created', CURRENT_TIMESTAMP);

COMMIT;

Обе строки получат одинаковое время.

Это хорошо: заказ и запись аудита относятся к одному логическому действию. Не будет странной ситуации, где заказ создан в 12:00:00.001, а аудит в 12:00:00.087.

Для бизнес-данных такая стабильность часто важнее, чем «тикающее» время.

Если транзакция — это один смысловой шаг, то и отметка времени у этого шага должна быть одна.

now() — это то же самое

В PostgreSQL многие пишут now() вместо CURRENT_TIMESTAMP.

Например:

SELECT now();

В PostgreSQL now() — это фактически то же самое, что CURRENT_TIMESTAMP. Он тоже возвращает время старта транзакции.

То есть такой пример тоже даст одинаковое значение внутри транзакции:

BEGIN;

SELECT now();

SELECT pg_sleep(3);

SELECT now();

COMMIT;

Если вы ожидали, что now() будет «тикать», PostgreSQL вас удивит. Для тикающего времени нужна другая функция.

statement_timestamp(): время старта оператора

Есть ещё statement_timestamp().

Она возвращает время старта текущего SQL-оператора.

Пример:

SELECT statement_timestamp();

Разница с CURRENT_TIMESTAMP проявляется внутри длинной транзакции.

BEGIN;

SELECT CURRENT_TIMESTAMP, statement_timestamp();

SELECT pg_sleep(3);

SELECT CURRENT_TIMESTAMP, statement_timestamp();

COMMIT;

CURRENT_TIMESTAMP останется временем старта транзакции.

statement_timestamp() во втором SELECT покажет время старта второго оператора.

То есть statement_timestamp() отвечает на вопрос:

когда начался именно этот SQL-запрос?

А CURRENT_TIMESTAMP отвечает на вопрос:

когда началась вся транзакция?

clock_timestamp(): настоящее тикающее время

Если вам нужно реальное «сейчас», которое меняется при каждом вызове, используйте clock_timestamp().

SELECT clock_timestamp();

Эта функция показывает текущее время по часам в момент вызова.

Даже внутри одного запроса она может вернуть разные значения:

SELECT
    n,
    clock_timestamp() AS row_time
FROM generate_series(1, 3) AS n;

clock_timestamp() полезна для измерений, диагностики и профилирования.

Например, внутри функции можно замерить, сколько занял отдельный шаг. А вот для обычных created_at и updated_at чаще лучше использовать CURRENT_TIMESTAMP, потому что там важна стабильная бизнес-отметка.

Сравнение функций времени в PostgreSQL

Удобно держать в голове такую таблицу:

Функция Что возвращает Меняется внутри транзакции
CURRENT_TIMESTAMP время старта транзакции нет
transaction_timestamp() время старта транзакции нет
now() время старта транзакции нет
LOCALTIMESTAMP локальное время старта транзакции без зоны нет
statement_timestamp() время старта текущего SQL-оператора да, между операторами
clock_timestamp() реальное текущее время да, при каждом вызове

Главное практическое правило:

для бизнес-событий и аудита используйте CURRENT_TIMESTAMP, для измерения времени выполнения — clock_timestamp().

Почему не стоит измерять длительность через CURRENT_TIMESTAMP

Допустим, вы хотите измерить, сколько занял кусок работы внутри транзакции.

Наивный вариант:

BEGIN;

SELECT CURRENT_TIMESTAMP AS started_at;

SELECT pg_sleep(3);

SELECT CURRENT_TIMESTAMP AS finished_at;

COMMIT;

Кажется, можно вычесть одно из другого и получить длительность. Но в PostgreSQL это не сработает: оба значения будут одинаковыми.

Для измерения длительности используйте clock_timestamp():

SELECT clock_timestamp() AS started_at;

SELECT pg_sleep(3);

SELECT clock_timestamp() AS finished_at;

А внутри одного запроса можно сделать так:

WITH t AS (
    SELECT clock_timestamp() AS started_at
)
SELECT
    clock_timestamp() - started_at AS elapsed
FROM t;

CURRENT_TIMESTAMP хорош для отметки события. clock_timestamp() хорош для секундомера.

Не путайте часы и секундомер — и запросы станут понятнее.

updated_at: почему DEFAULT недостаточно

Для created_at достаточно DEFAULT CURRENT_TIMESTAMP.

Но с updated_at есть нюанс.

DEFAULT срабатывает только при вставке строки. При обычном UPDATE он сам не обновится.

Например:

CREATE TABLE employees (
    id         bigserial PRIMARY KEY,
    name       text NOT NULL,
    manager_id bigint REFERENCES employees(id),
    dept       text,
    salary     numeric(12,2),
    created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Когда сотрудника вставили, обе колонки заполнились.

Но если потом поменять зарплату:

UPDATE employees
SET salary = 120000
WHERE id = 10;

updated_at сам по себе не изменится.

Для автоматического обновления в PostgreSQL обычно используют триггер.

Триггер для updated_at

Создадим функцию:

CREATE OR REPLACE FUNCTION touch_updated_at()
RETURNS trigger AS $$
BEGIN
    NEW.updated_at := CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Теперь создадим триггер:

CREATE TRIGGER trg_touch_employees
BEFORE UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION touch_updated_at();

После этого при каждом обновлении строки PostgreSQL будет автоматически менять updated_at.

Пример:

UPDATE employees
SET salary = 120000
WHERE id = 10;

Перед сохранением обновлённой строки сработает триггер, и updated_at получит новое значение CURRENT_TIMESTAMP.

Важно: если одним UPDATE обновить много строк, все они получат одну и ту же отметку времени, потому что CURRENT_TIMESTAMP стабилен в рамках транзакции. Для аудита это обычно удобно.

Когда для updated_at нужен clock_timestamp()

В большинстве случаев для updated_at достаточно CURRENT_TIMESTAMP.

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

Тогда в триггере можно использовать clock_timestamp():

CREATE OR REPLACE FUNCTION touch_updated_at()
RETURNS trigger AS $$
BEGIN
    NEW.updated_at := clock_timestamp();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Но это нужно не всегда.

Для бизнес-аудита чаще лучше, чтобы все изменения одной транзакции имели одну согласованную отметку. Поэтому CURRENT_TIMESTAMP остаётся хорошим выбором по умолчанию.

Тип колонки: timestamptz или timestamp

Для аудиторских колонок в PostgreSQL почти всегда выбирайте timestamptz.

Хороший вариант:

created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP

Менее удачный вариант для глобального приложения:

created_at timestamp NOT NULL DEFAULT LOCALTIMESTAMP

Почему второй вариант опаснее?

Потому что timestamp without time zone хранит просто дату и время без информации об абсолютном моменте.

Например, значение 2026-01-15 10:00:00 без зоны не говорит само по себе, это 10 утра где:

  • в Москве?
  • в Лиссабоне?
  • во Вьетнаме?
  • на сервере?
  • у пользователя?

Если приложение международное, такая неопределённость быстро приводит к ошибкам.

timestamptz хранит абсолютный момент и позволяет корректно показывать его в разных часовых поясах.

Как показать время в нужном часовом поясе

Допустим, в базе хранится timestamptz.

Показать его в конкретном часовом поясе можно через AT TIME ZONE.

SELECT
    created_at,
    created_at AT TIME ZONE 'Europe/Lisbon' AS created_at_lisbon,
    created_at AT TIME ZONE 'Europe/Moscow' AS created_at_moscow
FROM users
WHERE id = 42;

Это не меняет значение в таблице. Это только способ показать один и тот же момент в разных локальных часах.

Так удобно строить отчёты для пользователей из разных стран.

CURRENT_TIMESTAMP в INSERT

Обычно CURRENT_TIMESTAMP не нужно явно писать в INSERT, если у колонки уже есть DEFAULT.

Но технически можно:

INSERT INTO users (email, name, country, created_at)
VALUES ('ana@example.com', 'Ana', 'ES', CURRENT_TIMESTAMP);

Такой вариант полезен, если вы хотите явно показать момент создания в запросе.

Но в обычной схеме лучше оставить это базе:

INSERT INTO users (email, name, country)
VALUES ('ana@example.com', 'Ana', 'ES');

Чем меньше приложение вручную заполняет технические поля, тем меньше шансов забыть поле или передать неправильное время.

CURRENT_TIMESTAMP в аудите

Представим таблицу аудита:

CREATE TABLE order_audit (
    id         bigserial PRIMARY KEY,
    order_id   bigint NOT NULL,
    event      text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Теперь можно писать события так:

INSERT INTO order_audit (order_id, event)
VALUES (101, 'status_changed');

Дата события заполнится автоматически.

Если одно бизнес-действие создаёт несколько строк аудита в одной транзакции, все они получат одинаковый created_at. Это помогает потом понимать, какие записи относятся к одному логическому шагу.

Отличия от MySQL

В MySQL тоже есть CURRENT_TIMESTAMP, но детали отличаются.

Обычно его используют так:

CREATE TABLE users (
    id         bigint PRIMARY KEY AUTO_INCREMENT,
    email      varchar(255) NOT NULL UNIQUE,
    created_at timestamp DEFAULT CURRENT_TIMESTAMP,
    updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

В MySQL есть удобный синтаксис:

ON UPDATE CURRENT_TIMESTAMP

Он позволяет автоматически обновлять updated_at без отдельного триггера.

В PostgreSQL такого синтаксиса для обычной колонки нет, поэтому для updated_at чаще делают триггер.

Ещё важный момент: семантика времени в MySQL и PostgreSQL не полностью одинаковая. В PostgreSQL CURRENT_TIMESTAMP привязан к старту транзакции. В MySQL похожие функции чаще воспринимаются как время текущего оператора.

Поэтому при переносе SQL между базами нельзя просто копировать выражения и ждать абсолютно такого же поведения.

Проверяйте два вопроса:

  • будет ли время одинаковым внутри одной транзакции;
  • как именно обновляется updated_at.

Отличия от ClickHouse

В ClickHouse модель другая: это аналитическая колоночная база, а не классическая транзакционная СУБД вроде PostgreSQL.

Для текущего времени там часто используют now() или now64().

Пример:

SELECT
    now() AS current_time,
    now64() AS current_time_precise;

now() возвращает DateTime.

now64() используют, когда нужна более высокая точность, например миллисекунды.

Но привычной PostgreSQL-семантики «время старта транзакции» там нет в том же смысле, потому что сама модель транзакций другая.

Поэтому при переносе аналитических запросов из PostgreSQL в ClickHouse важно не только заменить функцию, но и понять, какой именно момент времени вы хотите получить.

Как выбрать правильную функцию

Для новичка удобнее всего идти от задачи.

Нужно записать, когда создана строка?

created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP

Нужно записать, когда строка обновлена?

Используйте updated_at с триггером и CURRENT_TIMESTAMP.

Нужно получить один стабильный момент для всей транзакции?

SELECT CURRENT_TIMESTAMP;

Нужно узнать время старта конкретного SQL-оператора?

SELECT statement_timestamp();

Нужно измерить реальное время выполнения?

SELECT clock_timestamp();

Нужны локальные часы без часового пояса?

SELECT LOCALTIMESTAMP;

Но для created_at и updated_at в большинстве приложений всё же лучше CURRENT_TIMESTAMP вместе с типом timestamptz.

Частая ошибка: ожидать, что now() тикает

В PostgreSQL такой код не показывает движение времени внутри транзакции:

BEGIN;

SELECT now();

SELECT pg_sleep(5);

SELECT now();

COMMIT;

Оба значения будут одинаковыми.

Это не баг. Это нормальное поведение PostgreSQL.

Если нужен секундомер, используйте:

SELECT clock_timestamp();

Если нужна стабильная отметка бизнес-события, используйте:

SELECT CURRENT_TIMESTAMP;

Частая ошибка: хранить локальное время без зоны

Ещё одна ошибка — делать аудиторские колонки типом timestamp without time zone.

Например:

created_at timestamp NOT NULL DEFAULT LOCALTIMESTAMP

Для маленького локального проекта это может быть терпимо. Но как только появляются пользователи из разных стран, серверы в разных регионах, переезды инфраструктуры или отчёты в разных часовых поясах, начинается путаница.

Лучше сразу хранить абсолютный момент:

created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP

А в нужный часовой пояс переводить уже при выводе.

Частая ошибка: ждать auto-update как в MySQL

В PostgreSQL такой столбец:

updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP

обновится автоматически только при вставке строки.

При UPDATE он сам не изменится.

Если нужно автоматическое обновление, добавляйте триггер:

CREATE TRIGGER trg_touch_employees
BEFORE UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION touch_updated_at();

Это важное отличие от MySQL, где часто используют ON UPDATE CURRENT_TIMESTAMP.

Главное из статьи

CURRENT_TIMESTAMP в PostgreSQL возвращает значение типа timestamp with time zone, то есть timestamptz.

Для колонок вроде created_at, updated_at, paid_at и других отметок реальных событий обычно выбирают именно timestamptz.

CURRENT_TIMESTAMP показывает время старта текущей транзакции. Внутри одной транзакции оно не меняется.

now() в PostgreSQL ведёт себя так же: это не тикающее время, а стабильное время транзакции.

LOCALTIMESTAMP возвращает timestamp without time zone, то есть локальные часы без привязки к абсолютному моменту.

statement_timestamp() показывает время старта текущего SQL-оператора.

clock_timestamp() показывает настоящее текущее время и может меняться при каждом вызове.

Для created_at обычно достаточно:

created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP

Для updated_at в PostgreSQL нужен триггер, потому что DEFAULT не срабатывает при обновлении строки.

Не измеряйте длительность через CURRENT_TIMESTAMP: внутри транзакции разница может быть нулевой. Для таймингов используйте clock_timestamp().

Главное правило простое:

CURRENT_TIMESTAMP — для стабильной отметки бизнес-события, clock_timestamp() — для настоящего тикающего времени.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр