sqlpostgresqlto-charformatting

TO_CHAR в PostgreSQL: как красиво форматировать даты, время и числа

TO_CHAR превращает даты, timestamp и числа в строки по текстовому шаблону: YYYY-MM-DD, HH24:MI, названия месяцев, разделители разрядов и отличия от MySQL.

9 мин чтенияСправочникsql · postgresql · to-char · formatting · dates · mysql

TO_CHAR — это функция PostgreSQL, которая превращает дату, время, интервал или число в строку по заданному шаблону.

Проще говоря, она отвечает за красивый «человеческий» вывод.

В базе дата может храниться так:

created_at
2026-06-17 14:32:45

А в отчёте хочется показать так:

created_date
17.06.2026

Или так:

created_time
14:32

Или так:

month
2026-06

Вот для таких задач и нужен TO_CHAR.

Он не меняет само значение в таблице. Он только берёт значение и показывает его в нужном текстовом виде.

Главное правило: TO_CHAR нужен для вывода

Синтаксис простой:

SELECT
  TO_CHAR(value, 'template') AS formatted_value
FROM table_name;

Первый аргумент — значение.

Второй аргумент — шаблон.

Результат всегда имеет тип text.

Например:

SELECT
  TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') AS created_at_text
FROM users;

Если created_at был равен 2026-06-17 14:32:45, результат будет строкой:

created_at_text
2026-06-17 14:32

Обратите внимание: после TO_CHAR это уже не дата и не время. Это текст.

И вот здесь начинается важный момент.

TO_CHAR лучше применять в самом конце запроса, когда вы уже всё отфильтровали, отсортировали, сгруппировали и посчитали. То есть в списке SELECT, чтобы красиво показать результат пользователю.

Плохая идея — превращать дату в строку слишком рано, а потом сортировать или фильтровать эту строку.

Почему не стоит сортировать по TO_CHAR

Представьте две даты:

real_date
2026-07-05
2026-06-17

Если превратить их в строки формата DD.MM.YYYY, получим:

date_text
05.07.2026
17.06.2026

Для человека всё понятно. Но для базы это теперь обычный текст.

Если сортировать строки по алфавиту, 05.07.2026 окажется раньше 17.06.2026, хотя по настоящей дате июнь идёт раньше июля.

Вот так лучше не делать:

SELECT
  TO_CHAR(created_at, 'DD.MM.YYYY') AS created_date
FROM orders
ORDER BY TO_CHAR(created_at, 'DD.MM.YYYY');

Лучше сортировать по настоящей дате, а форматировать только вывод:

SELECT
  TO_CHAR(created_at, 'DD.MM.YYYY') AS created_date
FROM orders
ORDER BY created_at;

Правило простое:

в WHERE, JOIN и ORDER BY по возможности используйте исходные типы данных, а TO_CHAR оставляйте для красивого финального вывода.

Шаблоны для даты и времени

Шаблон TO_CHAR состоит из специальных кодов.

Например:

SELECT
  TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') AS created_at_text
FROM users
ORDER BY created_at
LIMIT 5;

Шаблон здесь такой:

YYYY-MM-DD HH24:MI

Он означает:

Код Что выводит
YYYY год из 4 цифр
YY год из 2 цифр
MM месяц числом от 01 до 12
DD день месяца
HH24 час в 24-часовом формате
HH12 час в 12-часовом формате
MI минуты
SS секунды
AM / PM обозначение до полудня или после полудня
Mon короткое название месяца
Month полное название месяца
Day полное название дня недели
Dy короткое название дня недели

Все дефисы, пробелы, точки и двоеточия вы расставляете сами.

Например:

SELECT
  TO_CHAR(created_at, 'DD.MM.YYYY') AS date_text,
  TO_CHAR(created_at, 'HH24:MI') AS time_text
FROM orders;

Так можно получить дату отдельно и время отдельно.

MM — месяц, MI — минуты

Это одна из самых частых ошибок новичков.

В PostgreSQL:

  • MM — месяц;
  • MI — минуты.

Если написать так:

SELECT
  TO_CHAR(created_at, 'HH24:MM') AS bad_time
FROM orders;

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

Правильно так:

SELECT
  TO_CHAR(created_at, 'HH24:MI') AS good_time
FROM orders;

Запомнить можно так:

MI — minutes, минуты.
MM — month, месяц.

Ошибка неприятная именно потому, что PostgreSQL не ругается. Для него MM — нормальный код. Просто вы получите не то, что хотели.

Несколько полезных форматов для отчётов

Дата в привычном виде:

SELECT
  TO_CHAR(created_at, 'DD.MM.YYYY') AS created_date
FROM orders;

Дата в формате, удобном для сортировки глазами:

SELECT
  TO_CHAR(created_at, 'YYYY-MM-DD') AS created_date
FROM orders;

Дата и время без секунд:

SELECT
  TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') AS created_at_text
FROM orders;

Только месяц для отчёта:

SELECT
  TO_CHAR(created_at, 'YYYY-MM') AS month_text
FROM orders;

Время в 12-часовом формате:

SELECT
  TO_CHAR(created_at, 'HH12:MI AM') AS time_text
FROM orders;

Для отчётов чаще всего хватает этих шаблонов. Главное — не путать формат для показа и значение для вычислений.

Названия месяцев и дней недели

TO_CHAR умеет выводить не только числа, но и текстовые названия месяцев и дней недели.

Например:

SELECT
  TO_CHAR(created_at, 'Day') AS weekday_name,
  TO_CHAR(created_at, 'Month') AS month_name,
  TO_CHAR(created_at, 'Mon') AS short_month_name
FROM orders;

Регистр шаблона влияет на регистр результата.

Шаблон Пример результата
Day Monday
DAY MONDAY
day monday
Month June
MONTH JUNE
month june

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

Зачем нужен FM

У текстовых названий есть одна неожиданность: PostgreSQL может дополнять их пробелами до фиксированной ширины.

Например, названия месяцев имеют разную длину: May короткий, September длинный. Чтобы формат был ровный, PostgreSQL может добавлять пробелы.

В обычном отчёте эти пробелы чаще мешают.

Для их удаления используют модификатор FM.

SELECT
  TO_CHAR(created_at, 'FMDay, DD FMMonth YYYY') AS human_date
FROM orders;

FM убирает лишние пробелы у следующего элемента шаблона.

Без FM результат может выглядеть так, будто в строке внезапно появились лишние отступы. С FM формат становится аккуратнее.

Локализация названий месяцев

Язык названий дней и месяцев зависит от настроек PostgreSQL и операционной системы, в частности от параметра lc_time.

Если база настроена на английскую локаль, вы увидите английские названия.

Если нужна локализация, используют префикс TM.

SELECT
  TO_CHAR(created_at, 'TMDay, DD TMMonth YYYY') AS localized_date
FROM orders;

TM означает translate mode: PostgreSQL пытается вывести локализованное название.

Но важно понимать: сам по себе TM не «включает русский язык магически». Он работает вместе с настройками локали. Если на сервере не настроена нужная локаль, результат может остаться английским.

В реальных проектах это частая история: на локальной машине месяц выводится по-русски, а на сервере — по-английски. Поэтому формат с названиями месяцев лучше проверять именно в той среде, где будет работать отчёт.

Форматирование чисел через TO_CHAR

TO_CHAR умеет форматировать не только даты, но и числа.

Например, сумма заказа хранится как число:

amount
1250

А в отчёте хочется показать:

amount_text
1,250.00

Для этого используют числовую маску.

SELECT
  TO_CHAR(amount, 'FM999G999G990D00') AS amount_text
FROM orders;

Маска выглядит страшновато, но читается по частям.

Символ Что означает
9 цифра, но ведущий ноль не обязателен
0 цифра, ноль обязателен
D десятичный разделитель
G разделитель групп разрядов
FM убрать лишние пробелы
L символ валюты
S знак числа

Например:

SELECT
  TO_CHAR(id, '0000') AS padded_id
FROM orders;

Если id = 42, результат будет:

padded_id
0042

Это удобно для номеров, кодов, чеков и других значений, где нужна фиксированная длина.

Разница между 9 и 0

9 говорит:

здесь может быть цифра, но если ведущей цифры нет, не обязательно показывать ноль.

0 говорит:

здесь обязательно должна быть цифра, если её нет — поставь ноль.

Посмотрим на идею.

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

SELECT
  TO_CHAR(amount, 'FM999990D00') AS amount_text
FROM orders;

Так сумма 15 превратится в строку с двумя знаками после разделителя.

Если вы форматируете деньги, обычно стоит явно задавать дробную часть через 00.

Почему для чисел часто нужен FM

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

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

В отчётах это часто раздражает: значения визуально «съезжают».

Поэтому для денежных сумм и обычного пользовательского вывода часто пишут так:

SELECT
  TO_CHAR(amount, 'FM999G999G990D00') AS amount_text
FROM orders;

FM убирает лишнее заполнение пробелами.

Если число не помещается в шаблон

У числовой маски есть ширина.

Если значение слишком большое и не помещается в шаблон, PostgreSQL вернёт строку из символов #.

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

Это не баг. Это сигнал:

в шаблоне не хватает места для всех цифр.

Решение простое: расширить маску.

Было:

SELECT
  TO_CHAR(amount, 'FM999D00') AS amount_text
FROM orders;

Стало:

SELECT
  TO_CHAR(amount, 'FM999G999G990D00') AS amount_text
FROM orders;

Для денег лучше сразу закладывать запас по количеству разрядов.

TO_CHAR и NULL

Если передать в TO_CHAR значение NULL, результатом будет NULL.

SELECT
  TO_CHAR(paid_at, 'DD.MM.YYYY') AS paid_date
FROM orders;

Если заказ ещё не оплачен и paid_at равен NULL, в результате тоже будет NULL.

В отчёте иногда хочется показать текст-заглушку.

Тогда используйте COALESCE.

SELECT
  COALESCE(TO_CHAR(paid_at, 'DD.MM.YYYY'), 'not paid') AS paid_date
FROM orders;

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

TO_CHAR и GROUP BY

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

Например, по месяцу:

SELECT
  TO_CHAR(created_at, 'YYYY-MM') AS month_text,
  COUNT(*) AS orders_count
FROM orders
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month_text;

Такой запрос может работать нормально, потому что формат YYYY-MM сортируется как строка в правильном порядке.

Но всё равно часто лучше группировать по настоящей дате, обрезанной до месяца, а форматировать только результат.

SELECT
  TO_CHAR(date_trunc('month', created_at), 'YYYY-MM') AS month_text,
  COUNT(*) AS orders_count
FROM orders
GROUP BY date_trunc('month', created_at)
ORDER BY date_trunc('month', created_at);

Здесь логика чище:

  • date_trunc отвечает за группировку по месяцу;
  • TO_CHAR отвечает только за красивую подпись месяца.

Это хороший стиль для аналитических запросов.

TO_CHAR в WHERE может мешать индексам

Допустим, на колонке created_at есть обычный индекс.

Если написать так:

SELECT
  id,
  created_at
FROM orders
WHERE TO_CHAR(created_at, 'YYYY-MM') = '2026-06';

PostgreSQL придётся применить функцию к значениям created_at, чтобы сравнить результат со строкой. Обычный индекс по created_at в такой ситуации может оказаться бесполезным.

Лучше фильтровать по диапазону дат:

SELECT
  id,
  created_at
FROM orders
WHERE created_at >= DATE '2026-06-01'
  AND created_at < DATE '2026-07-01';

Такой запрос понятнее для базы и обычно лучше дружит с индексом.

А уже в SELECT можно красиво вывести месяц:

SELECT
  id,
  TO_CHAR(created_at, 'YYYY-MM') AS month_text
FROM orders
WHERE created_at >= DATE '2026-06-01'
  AND created_at < DATE '2026-07-01';

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

фильтруйте датами как датами, числами как числами, а форматируйте только то, что показываете.

Когда TO_CHAR действительно уместен

TO_CHAR хорошо подходит для финального слоя отчёта.

Например:

SELECT
  o.id,
  TO_CHAR(o.created_at, 'DD.MM.YYYY HH24:MI') AS created_at_text,
  TO_CHAR(o.amount, 'FM999G999G990D00') AS amount_text
FROM orders o
WHERE o.status = 'paid'
ORDER BY o.created_at DESC;

Здесь всё на своих местах:

  • WHERE фильтрует по обычной колонке;
  • ORDER BY сортирует по настоящему времени;
  • TO_CHAR только красиво показывает дату и сумму.

Это именно тот сценарий, где функция раскрывается лучше всего.

Чем PostgreSQL отличается от MySQL

В MySQL для дат обычно используют не TO_CHAR, а DATE_FORMAT.

И шаблоны там другие.

PostgreSQL:

SELECT
  TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') AS created_at_text
FROM users;

MySQL:

SELECT
  DATE_FORMAT(created_at, '%Y-%m-%d %H:%i') AS created_at_text
FROM users;

Несколько важных отличий:

Что нужно PostgreSQL MySQL
Год из 4 цифр YYYY %Y
Месяц числом MM %m
День месяца DD %d
Час 24 HH24 %H
Минуты MI %i
Полное название месяца Month %M
Короткое название месяца Mon %b

То есть шаблон из PostgreSQL нельзя просто перенести в MySQL. Его нужно переписать.

Для чисел в MySQL часто используют отдельную функцию FORMAT, а не TO_CHAR.

ClickHouse тоже использует другой синтаксис

В ClickHouse для форматирования даты и времени часто используют formatDateTime.

Пример:

SELECT
  formatDateTime(created_at, '%Y-%m-%d %H:%M') AS created_at_text
FROM users;

Выглядит похоже на MySQL, но и здесь есть свои нюансы.

Одна из ловушек: в разных СУБД одинаковые буквы в шаблонах могут означать разные вещи. Поэтому при переезде между PostgreSQL, MySQL и ClickHouse формат даты лучше проверять по документации и тестовому запросу, а не переносить на глаз.

Главная мысль:

шаблоны форматирования почти всегда завязаны на конкретную СУБД.

Частые ошибки

Первая ошибка — использовать TO_CHAR для сортировки даты.

SELECT
  TO_CHAR(created_at, 'DD.MM.YYYY') AS created_date
FROM orders
ORDER BY TO_CHAR(created_at, 'DD.MM.YYYY');

Лучше сортировать по исходной колонке:

SELECT
  TO_CHAR(created_at, 'DD.MM.YYYY') AS created_date
FROM orders
ORDER BY created_at;

Вторая ошибка — путать месяц и минуты.

SELECT
  TO_CHAR(created_at, 'HH24:MM') AS time_text
FROM orders;

Правильно:

SELECT
  TO_CHAR(created_at, 'HH24:MI') AS time_text
FROM orders;

Третья ошибка — забывать, что результат TO_CHAR это text.

SELECT
  TO_CHAR(amount, 'FM999G999D00') AS amount_text
FROM orders
ORDER BY amount_text;

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

SELECT
  TO_CHAR(amount, 'FM999G999D00') AS amount_text
FROM orders
ORDER BY amount;

Четвёртая ошибка — не учитывать NULL.

SELECT
  TO_CHAR(paid_at, 'DD.MM.YYYY') AS paid_date
FROM orders;

Если нужна заглушка:

SELECT
  COALESCE(TO_CHAR(paid_at, 'DD.MM.YYYY'), 'not paid') AS paid_date
FROM orders;

Пятая ошибка — делать фильтр по месяцу через строку.

SELECT
  id
FROM orders
WHERE TO_CHAR(created_at, 'YYYY-MM') = '2026-06';

Лучше фильтровать диапазоном:

SELECT
  id
FROM orders
WHERE created_at >= DATE '2026-06-01'
  AND created_at < DATE '2026-07-01';

Главное

TO_CHAR в PostgreSQL превращает дату, время, интервал или число в строку по шаблону.

Для даты и времени:

SELECT
  TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') AS created_at_text
FROM orders;

Для чисел:

SELECT
  TO_CHAR(amount, 'FM999G999G990D00') AS amount_text
FROM orders;

Результат TO_CHAR всегда имеет тип text. Поэтому функцию лучше использовать для финального вывода, а не для основной логики запроса.

Хороший стиль:

SELECT
  TO_CHAR(created_at, 'DD.MM.YYYY') AS created_date
FROM orders
WHERE created_at >= DATE '2026-06-01'
  AND created_at < DATE '2026-07-01'
ORDER BY created_at;

Здесь база фильтрует и сортирует по настоящей дате, а TO_CHAR только красиво показывает результат.

Запомните три главные ловушки:

  • MM — месяц, MI — минуты;
  • после TO_CHAR значение становится текстом;
  • форматирование лучше делать в конце запроса.

Если держать это в голове, TO_CHAR станет удобным инструментом для аккуратных отчётов: даты будут выглядеть понятно, время — без лишних секунд, суммы — с разрядами и копейками, а SQL-запрос останется честным и предсказуемым.

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

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

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