Строки, текст, дата и время, таймзоны

Рейтинг: 62.1% · 15 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Строки, текст, дата и время, таймзоны

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны (вы здесь)
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 14. Строки, текст, дата и время, таймзоны

В прошлых уроках мы разобрали числа и базовые типы. Теперь беремся за два семейства, которые приносят больше всего боли на практике: строки и моменты времени. Разберем, чем отличаются char, varchar и text, как кодировка и COLLATE влияют на сортировку и поиск, какие функции реально нужны для работы со строками. Потом перейдем к датам и времени: date, time, timestamp, timestamptz, интервалы и главный источник граблей в любом проекте на postgresql - таймзоны. По ходу будем гонять запросы на демобазе Авиаперевозки от Postgres Professional, где как раз полно полей со временем вылета.

Изображение

Как это работает

Начнем со строк. В PostgreSQL есть три текстовых типа, но внутри они почти близнецы. char(n) - строка фиксированной длины, которая дополняется пробелами справа до n символов. varchar(n) - строка с ограничением сверху на длину. text - строка без ограничения длины. Важная мысль: varchar(n) и text хранятся одинаково и работают с одинаковой скоростью, ограничение длины - это просто проверка при вставке, а не способ сэкономить место. Поэтому в современных схемах char почти не используют (он только мешает лишними пробелами), а выбор обычно стоит между varchar(n) ради смысловой валидации и text для всего остального.

Длина строки в PostgreSQL считается в символах, а не байтах, и это второй ключевой момент. Кодировку задает база при создании, и стандарт на 2026 год - UTF-8. В UTF-8 латинская буква занимает 1 байт, а кириллическая - 2 байта. Поэтому char_length дает число символов, а octet_length - число байт, и для русского текста они различаются вдвое. Если перепутать их в проверке лимита поля, можно неожиданно обрезать данные.

Сортировка и сравнение строк зависят от правила сопоставления - COLLATE. Это набор правил конкретного языка: где какая буква стоит по алфавиту, учитывается ли регистр. Сравнение abc < ABC может дать разный результат в зависимости от collation. По умолчанию берется правило базы данных, но COLLATE можно навесить точечно в ORDER BY или прямо на столбец. С версии 15 PostgreSQL умеет ICU-коллации как основные для базы, и это более стабильное и предсказуемое поведение, чем у старых системных libc-локалей.

Теперь время. Тут важно держать в голове одно разделение. timestamp (он же timestamp without time zone) хранит дату и время как есть, без привязки к поясу - просто числа на стене часов. timestamptz (timestamp with time zone) хранит момент абсолютного времени: внутри это всегда UTC, а пояс используется только при вводе и выводе. То есть timestamptz не хранит вашу таймзону - он приводит входное значение к UTC и потом показывает его в поясе текущей сессии. Это и есть главная развилка всего урока.

SQL и примеры

Сначала строки. Посмотрим разницу длины в символах и байтах:

Код: Выделить всё

SELECT char_length('Привет') AS chars,   -- 6 символов
       octet_length('Привет') AS bytes;  -- 12 байт в UTF-8
Базовый набор строковых функций, который стоит знать наизусть. Здесь же показано, что char(n) добавляет хвост из пробелов:

Код: Выделить всё

SELECT upper('aero') AS up,                  -- AERO
       lower('SVO') AS low,                  -- svo
       left('Москва', 3) AS l,               -- Мос
       substring('SVO-LED' FROM 5) AS sub,   -- LED
       trim('  hi  ') AS trimmed,            -- hi
       'SVO' || '-' || 'LED' AS concat,      -- SVO-LED
       length('abc'::char(8)) AS char_len;   -- 3, хвостовые пробелы не в счет
Поиск по образцу: LIKE учитывает регистр, ILIKE - нет. Найдем аэропорты, в названии города которых встречается Москва (поле city в airports - это тип jsonb с переводами, но для примера возьмем текстовое представление):

Код: Выделить всё

SELECT airport_code, city
FROM   airports
WHERE  city->>'ru' ILIKE '%москв%';
Сортировка с явной коллацией. Сравним порядок по русскому правилу:

Код: Выделить всё

SELECT airport_name->>'ru' AS name
FROM   airports
ORDER BY airport_name->>'ru' COLLATE "ru-RU-x-icu";
Переходим к времени. Демобаза хранит время вылета в двух полях: scheduled_departure и actual_departure, оба типа timestamptz. Посмотрим, на сколько реально задержали рейсы, через интервал-арифметику:

Код: Выделить всё

SELECT flight_id,
       actual_departure - scheduled_departure AS delay
FROM   flights
WHERE  actual_departure IS NOT NULL
ORDER BY delay DESC NULLS LAST
LIMIT 5;
Вычитание двух timestamptz дает interval - тип для длительностей. Его можно прибавлять и сравнивать. Текущий момент и усечение до часа:

Код: Выделить всё

SELECT now()                              AS moment,      -- момент с поясом
       date_trunc('hour', now())         AS hour_start,  -- начало часа
       extract(dow  FROM now())          AS day_of_week, -- 0=воскресенье
       extract(epoch FROM interval '90 minutes') AS secs; -- 5400
Сгруппируем вылеты по дням - частый аналитический запрос. date_trunc срезает время до начала суток:

Код: Выделить всё

SELECT date_trunc('day', scheduled_departure) AS day,
       count(*) AS flights
FROM   flights
GROUP  BY day
ORDER  BY day;
И главный фокус с таймзонами. Одно и то же абсолютное время показывается по-разному в зависимости от пояса сессии:

Код: Выделить всё

SET timezone = 'UTC';
SELECT scheduled_departure FROM flights WHERE flight_id = 1;
SET timezone = 'Europe/Moscow';
SELECT scheduled_departure FROM flights WHERE flight_id = 1;
-- значение в базе одно, на экране сдвиг на 3 часа
Явно перевести момент в нужный пояс можно оператором AT TIME ZONE:

Код: Выделить всё

SELECT scheduled_departure AT TIME ZONE 'Asia/Vladivostok' AS local_vvo
FROM   flights WHERE flight_id = 1;
Частые грабли
  • Колонка timestamp вместо timestamptz для событий. Без пояса вы потеряете информацию о моменте: данные с разных серверов смешаются, и при смене пояса сервиса все поедет. Для абсолютных событий почти всегда нужен timestamptz.
  • Иллюзия, что timestamptz хранит таймзону. Он хранит UTC. Если вам нужно запомнить именно исходный пояс пользователя - заводите отдельную колонку с его именем.
  • char(n) и невидимые пробелы. Сравнения и конкатенация с дополненной пробелами строкой дают сюрпризы. Не используйте char без явной причины.
  • Путаница char_length и octet_length для кириллицы. Лимит поля в байтах при UTF-8 урежет вдвое меньше русских символов, чем кажется.
  • now() внутри транзакции возвращает время старта транзакции, а не реальный текущий момент. Для настоящего стенного времени берите clock_timestamp().
  • Сравнение timestamptz и timestamp в одном выражении: PostgreSQL неявно докрутит timestamp к поясу сессии, и результат зависит от текущего timezone. Приводите типы явно.
  • Сортировка строк ломается при смене locale базы. Не полагайтесь на порядок по умолчанию для бизнес-логики - задавайте COLLATE явно там, где порядок важен.
Мини-лаба
  • Создайте таблицу: CREATE TABLE tz_demo (id int GENERATED ALWAYS AS IDENTITY, label text, ts timestamp, tstz timestamptz);
  • Вставьте одну строку со значением 2026-06-14 12:00:00+03 в оба поля ts и tstz.
  • Выполните SET timezone = 'UTC'; и сделайте SELECT - запишите, что показали ts и tstz.
  • Выполните SET timezone = 'Europe/Moscow'; повторите SELECT и сравните: какое поле изменилось, а какое нет, и объясните почему.
  • Посчитайте char_length и octet_length для строки из вашего имени на русском.
  • Через date_trunc и extract достаньте из tstz начало суток и номер дня недели.
  • Удалите таблицу: DROP TABLE tz_demo;
Контрольные вопросы
  • Чем varchar(n) отличается от text по хранению и по поведению, и когда оправдан char(n)?
  • Почему для русского текста char_length и octet_length дают разные числа?
  • Что физически хранит timestamptz и в какой момент применяется таймзона?
  • Чем now() отличается от clock_timestamp() внутри транзакции?
  • Зачем нужен COLLATE и как он влияет на ORDER BY и сравнение строк?
  • Что делает оператор AT TIME ZONE с типом timestamptz?
👍7 ❤️4 🔥 😄 🤔1
Аватара пользователя
Yxcvbn
Сообщения: 1
Зарегистрирован: 15 май 2026, 20:16

Re: Строки, текст, дата и время, таймзоны

Сообщение Yxcvbn »

А если у меня колонка уже timestamp без зоны и данных уже миллион строк - как безопасно перевести в timestamptz, чтобы старые значения не сдвинулись на 3 часа?
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
alex1967
Сообщения: 1
Зарегистрирован: 27 май 2026, 12:16

Re: Строки, текст, дата и время, таймзоны

Сообщение alex1967 »

Поймал на проекте для 1с: лимит поля задал в символах, а драйвер считал в байтах, кириллица обрезалась ровно посередине. Теперь везде text без длины и проверка на стороне приложения.
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Числовые и булевы типы, NULL и трёхзначная логика
Следующая глава →
Массивы, диапазоны, enum, составные типы

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя