Глава 2 из 28
2. Типы данных: числа, текст, даты, boolean, uuid
Тип колонки определяет, какие значения в ней можно хранить, сколько места они займут и какие операции с ними допустимы. Неудачный выбор типа сложно исправить потом: смена типа на большой таблице означает её переписывание. В этой главе разберём типы, которые нужны в девяти случаях из десяти, и ловушки в каждом.
Все примеры запускаются кнопкой «Выполнить». Для них не нужны таблицы: хватает SELECT с литералами.
Целые числа: int или bigint
| Тип | Размер | Диапазон |
|---|---|---|
smallint | 2 байта | ±32 767 |
integer (int) | 4 байта | ±2,1 млрд |
bigint | 8 байт | ±9,2 · 10¹⁸ |
Для идентификаторов и счётчиков обычно берут int, а когда строк может стать больше двух миллиардов, сразу bigint. Переполнение случается неожиданно: счётчик заказов, за пять лет дошедший до двух миллиардов, останавливает приложение ошибкой.
SELECT 2147483647::int AS max_int;
SELECT 2147483647::int + 1 AS overflow;
ERROR: integer out of range
Второй запрос завершится ошибкой, а не вернёт отрицательное число, как во многих языках программирования. Если сомневаетесь, берите bigint: разница в 4 байта на строку обычно незаметна.
Деньги и точные числа: numeric
Числа с дробной частью бывают двух видов. Типы real и double precision хранят приближённое значение. numeric(точность, масштаб) хранит число точно.
SELECT 0.1::double precision + 0.2::double precision AS float_sum,
0.1::numeric + 0.2::numeric AS numeric_sum;
float_sum | numeric_sum
---------------------+-------------
0.30000000000000004 | 0.3
Двоичное представление не может точно записать 0,1, поэтому сумма двух «простых» чисел отличается от 0,3. Для денег, процентов и всего, что нужно сводить до копейки, используйте numeric: numeric(12, 2) — до 12 цифр, из них 2 после запятой. Тип money лучше обходить: он привязан к настройке локали сервера.
У numeric есть цена: он медленнее double precision и занимает больше места. Для научных расчётов и координат точность до копейки не нужна, берите double precision.
Ещё одна ловушка: деление целых чисел отбрасывает дробную часть.
SELECT 7 / 2 AS int_division, 7 / 2.0 AS exact_division;
int_division | exact_division
--------------+--------------------
3 | 3.5000000000000000
Текст: text, varchar, char
В PostgreSQL три текстовых типа, и между text и varchar нет разницы в скорости.
text— строка любой длины. Берите его по умолчанию.varchar(n)— строка доnсимволов. Ограничение длины полезно, когда оно есть в предметной области (код страны из двух букв).char(n)— строка фиксированной длины, добитая пробелами. Почти всегда не нужна.
SELECT 'abc'::char(6) = 'abc'::char(6) AS same,
length('abc'::char(6)) AS len_char,
length('abc'::varchar(6)) AS len_varchar;
same | len_char | len_varchar
------+----------+-------------
t | 3 | 3
Значение, которое не помещается, при вставке в колонку вызывает ошибку, а не обрезается молча:
CREATE TABLE codes (code varchar(5));
INSERT INTO codes VALUES ('слишком длинное');
ERROR: value too long for type character varying(5)
Осторожно: явное приведение типа ведёт себя иначе. 'слишком длинное'::varchar(5) не вызовет ошибку, а тихо оставит первые пять символов. Поэтому ограничения длины надёжнее задавать в колонках таблицы.
Регистр в сравнении строк имеет значение: 'Anna' = 'anna' даёт ложь. Для поиска без учёта регистра применяют lower(...) или ILIKE.
Даты и время: date, timestamp, timestamptz
| Тип | Что хранит |
|---|---|
date | только дату: 2026-10-15 |
time | только время суток |
timestamp | дату и время без часового пояса |
timestamptz | момент времени, с учётом часового пояса |
interval | промежуток: «3 дня», «2 часа» |
Главное правило: для моментов событий берите timestamptz. Заказ создан, платёж прошёл, пользователь вошёл — это конкретные моменты, и их нужно однозначно сопоставлять, где бы ни находился читатель. Несмотря на название, timestamptz не хранит сам часовой пояс. Он приводит значение к UTC при записи и переводит в пояс сессии при чтении.
SET timezone = 'UTC';
SELECT ('2026-10-15 12:00:00+03'::timestamptz)::text AS moment;
moment
------------------------
2026-10-15 09:00:00+00
SET timezone = 'Asia/Vladivostok';
SELECT ('2026-10-15 12:00:00+03'::timestamptz)::text AS moment;
moment
------------------------
2026-10-15 19:00:00+10
Это один и тот же момент, показанный в двух поясах. Тип timestamp без пояса так не умеет: он просто хранит «12:00» без указания, где, и тот же запрос на сервере в другом поясе даст другой результат.
С датами работают через арифметику и интервалы:
SELECT date '2026-10-15' + 30 AS plus_30_days,
date '2026-10-15' + interval '1 month' AS plus_1_month,
date '2026-12-31' - date '2026-10-15' AS days_between;
plus_30_days | plus_1_month | days_between
--------------+---------------------+--------------
2026-11-14 | 2026-11-15 00:00:00 | 77
Обратите внимание: date + interval возвращает timestamp, а date + число остаётся датой, число считается днями.
Для группировок удобна date_trunc, она обрезает время до нужной точности:
SELECT date_trunc('month', timestamp '2026-10-15 17:42:00') AS month_start,
date_trunc('day', timestamp '2026-10-15 17:42:00') AS day_start;
month_start | day_start
---------------------+---------------------
2026-10-01 00:00:00 | 2026-10-15 00:00:00
boolean
Тип boolean принимает true, false и NULL. Третье значение важно: «неизвестно» не равно «ложь», и запрос WHERE active отбросит строки и с false, и с NULL.
SELECT true AND NULL AS and_null,
false AND NULL AS false_and_null,
true OR NULL AS or_null;
and_null | false_and_null | or_null
----------+----------------+---------
| f | t
true AND NULL — неизвестно, потому что итог зависит от неизвестного значения. А false AND NULL точно ложь при любом значении справа. Подробнее о NULL — в главе про запросы. Если ждёте от колонки только «да» или «нет», добавьте NOT NULL.
uuid
uuid — 128-битный идентификатор, который можно генерировать на любой стороне без обращения к базе и без риска совпадений. Он удобен, когда идентификаторы создают клиенты или несколько сервисов, и когда не хочется выдавать наружу последовательные номера.
SELECT gen_random_uuid() AS id,
pg_typeof(gen_random_uuid()) AS type;
Функция gen_random_uuid() встроена с версии 13. Цена uuid: он занимает 16 байт вместо 4 или 8 у int/bigint, а случайные значения хуже ложатся в индекс, чем растущие числа. Для внутренних идентификаторов, которые не покидают систему, bigint почти всегда дешевле.
Проверка типа: приведение и ошибки
Привести значение к типу можно записью значение::тип или функцией CAST. Если значение не подходит, это ошибка, а не молчаливое «ноль»:
SELECT '42'::int AS ok, CAST('3.14' AS numeric) AS also_ok;
SELECT 'abc'::int;
ERROR: invalid input syntax for type integer: "abc"
Тип любого выражения показывает pg_typeof(...). Это самый быстрый способ понять, почему запрос ведёт себя не так, как вы ожидали.
Частые ошибки
- Хранить деньги в
realилиdouble precision. Копейки «плывут». Беритеnumeric(12, 2)или храните целое число копеек. - Использовать
timestampбез пояса для моментов событий. Время потеряет смысл, как только серверы или пользователи окажутся в разных поясах. - Хранить даты и числа в
text. Сортировка превратится в алфавитную ('10' < '9'), а арифметика не заработает. - Брать
intдля счётчиков, которые могут вырасти. Переполнение останавливает запись. Смена типа на большой таблице болезненна. - Брать
char(n)«для надёжности». Он добивает значения пробелами и ведёт себя неочевидно. Достаточноtextилиvarchar(n). - Делить целые числа и ждать дробь.
7 / 2даёт 3. Приведите один из операндов:7 / 2.0.
Типы данных разобраны на заданиях в курсе «SQL с нуля».
Откройте остальные главы
Ещё 20 глав: внутреннее устройство, оптимизация, администрирование. Покупка разовая, доступ навсегда.