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() — для настоящего тикающего времени.
CURRENT_TIMESTAMP— это стандартная SQL-функция для получения текущей даты и времени.На первый взгляд всё просто: нужно записать, когда пользователь зарегистрировался, когда создали заказ или когда произошло событие в аудите, — ставим
CURRENT_TIMESTAMP.Но в PostgreSQL у этой функции есть важная особенность:
Именно из-за этого начинающие часто удивляются: между двумя запросами прошло несколько секунд, а время осталось одинаковым.
Разберём спокойно:
CURRENT_TIMESTAMP;LOCALTIMESTAMP;clock_timestamp();created_atиupdated_at;Базовый пример
Самый простой вызов:
SELECT CURRENT_TIMESTAMP;В PostgreSQL результат будет иметь тип
timestamp with time zone, его часто сокращают доtimestamptz.Пример результата:
Здесь есть дата, время, дробные секунды и часовой пояс вывода.
Важно понимать:
timestamptzхранит не «время с приклеенной строкой часового пояса», а абсолютный момент времени. PostgreSQL хранит момент, а показывает его с учётом текущей настройки часового пояса сессии.Для бизнес-приложений это почти всегда то, что нужно.
Если пользователь из Москвы и пользователь из Лиссабона видят один и тот же заказ, в базе должен храниться один абсолютный момент создания заказа, а не две разные локальные даты.
CURRENT_TIMESTAMP и LOCALTIMESTAMP
В PostgreSQL рядом с
CURRENT_TIMESTAMPесть похожая функцияLOCALTIMESTAMP.Сравним их:
SELECT CURRENT_TIMESTAMP AS with_zone, LOCALTIMESTAMP AS without_zone;Разница в типах:
CURRENT_TIMESTAMPtimestamp with time zoneLOCALTIMESTAMPtimestamp without time zoneCURRENT_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_TIMESTAMP2026-01-15 12:34:56.789123+03CURRENT_TIMESTAMP(0)2026-01-15 12:34:57+03CURRENT_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()отвечает на вопрос:А
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_TIMESTAMPtransaction_timestamp()now()LOCALTIMESTAMPstatement_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().Главное правило простое: