TO_CHAR — это функция PostgreSQL, которая превращает дату, время, интервал или число в строку по заданному шаблону.
Проще говоря, она отвечает за красивый «человеческий» вывод.
В базе дата может храниться так:
| created_at |
2026-06-17 14:32:45 |
А в отчёте хочется показать так:
Или так:
Или так:
Вот для таких задач и нужен 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:
Если написать так:
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 умеет форматировать не только даты, но и числа.
Например, сумма заказа хранится как число:
А в отчёте хочется показать:
Для этого используют числовую маску.
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, результат будет:
Это удобно для номеров, кодов, чеков и других значений, где нужна фиксированная длина.
Разница между 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-запрос останется честным и предсказуемым.
TO_CHAR— это функция PostgreSQL, которая превращает дату, время, интервал или число в строку по заданному шаблону.Проще говоря, она отвечает за красивый «человеческий» вывод.
В базе дата может храниться так:
2026-06-17 14:32:45А в отчёте хочется показать так:
17.06.2026Или так:
14:32Или так:
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, результат будет строкой:2026-06-17 14:32Обратите внимание: после
TO_CHARэто уже не дата и не время. Это текст.И вот здесь начинается важный момент.
TO_CHARлучше применять в самом конце запроса, когда вы уже всё отфильтровали, отсортировали, сгруппировали и посчитали. То есть в спискеSELECT, чтобы красиво показать результат пользователю.Плохая идея — превращать дату в строку слишком рано, а потом сортировать или фильтровать эту строку.
Почему не стоит сортировать по TO_CHAR
Представьте две даты:
2026-07-052026-06-17Если превратить их в строки формата
DD.MM.YYYY, получим:05.07.202617.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;Правило простое:
Шаблоны для даты и времени
Шаблон
TO_CHARсостоит из специальных кодов.Например:
SELECT TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') AS created_at_text FROM users ORDER BY created_at LIMIT 5;Шаблон здесь такой:
Он означает:
YYYYYYMM01до12DDHH24HH12MISSAM/PMMonMonthDayDyВсе дефисы, пробелы, точки и двоеточия вы расставляете сами.
Например:
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;Запомнить можно так:
Ошибка неприятная именно потому, что 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;Регистр шаблона влияет на регистр результата.
DayMondayDAYMONDAYdaymondayMonthJuneMONTHJUNEmonthjuneТо есть 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умеет форматировать не только даты, но и числа.Например, сумма заказа хранится как число:
1250А в отчёте хочется показать:
1,250.00Для этого используют числовую маску.
SELECT TO_CHAR(amount, 'FM999G999G990D00') AS amount_text FROM orders;Маска выглядит страшновато, но читается по частям.
90DGFMLSНапример:
SELECT TO_CHAR(id, '0000') AS padded_id FROM orders;Если
id = 42, результат будет:0042Это удобно для номеров, кодов, чеков и других значений, где нужна фиксированная длина.
Разница между 9 и 0
9говорит:0говорит:Посмотрим на идею.
Если нужно показать число с двумя знаками после десятичного разделителя, лучше использовать нули в дробной части:
SELECT TO_CHAR(amount, 'FM999990D00') AS amount_text FROM orders;Так сумма
15превратится в строку с двумя знаками после разделителя.Если вы форматируете деньги, обычно стоит явно задавать дробную часть через
00.Почему для чисел часто нужен FM
Без
FMPostgreSQL может добавлять ведущий пробел для положительных чисел. Это место резервируется под знак минуса.Например, положительное число может выглядеть так, будто перед ним есть лишний пробел.
В отчётах это часто раздражает: значения визуально «съезжают».
Поэтому для денежных сумм и обычного пользовательского вывода часто пишут так:
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;Несколько важных отличий:
YYYY%YMM%mDD%dHH24%HMI%iMonth%MMon%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-запрос останется честным и предсказуемым.