SQLLab

Глава 2 из 28

2. Типы данных: числа, текст, даты, boolean, uuid

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

Все примеры запускаются кнопкой «Выполнить». Для них не нужны таблицы: хватает SELECT с литералами.

Целые числа: int или bigint

ТипРазмерДиапазон
smallint2 байта±32 767
integer (int)4 байта±2,1 млрд
bigint8 байт±9,2 · 10¹⁸

Для идентификаторов и счётчиков обычно берут int, а когда строк может стать больше двух миллиардов, сразу bigint. Переполнение случается неожиданно: счётчик заказов, за пять лет дошедший до двух миллиардов, останавливает приложение ошибкой.

sql
SELECT 2147483647::int AS max_int;
sql
SELECT 2147483647::int + 1 AS overflow;
код
ERROR:  integer out of range

Второй запрос завершится ошибкой, а не вернёт отрицательное число, как во многих языках программирования. Если сомневаетесь, берите bigint: разница в 4 байта на строку обычно незаметна.

Деньги и точные числа: numeric

Числа с дробной частью бывают двух видов. Типы real и double precision хранят приближённое значение. numeric(точность, масштаб) хранит число точно.

sql
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.

Ещё одна ловушка: деление целых чисел отбрасывает дробную часть.

sql
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) — строка фиксированной длины, добитая пробелами. Почти всегда не нужна.
sql
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

Значение, которое не помещается, при вставке в колонку вызывает ошибку, а не обрезается молча:

sql
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 при записи и переводит в пояс сессии при чтении.

sql
SET timezone = 'UTC';
SELECT ('2026-10-15 12:00:00+03'::timestamptz)::text AS moment;
код
         moment
------------------------
 2026-10-15 09:00:00+00
sql
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» без указания, где, и тот же запрос на сервере в другом поясе даст другой результат.

С датами работают через арифметику и интервалы:

sql
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, она обрезает время до нужной точности:

sql
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.

sql
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-битный идентификатор, который можно генерировать на любой стороне без обращения к базе и без риска совпадений. Он удобен, когда идентификаторы создают клиенты или несколько сервисов, и когда не хочется выдавать наружу последовательные номера.

sql
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. Если значение не подходит, это ошибка, а не молчаливое «ноль»:

sql
SELECT '42'::int AS ok, CAST('3.14' AS numeric) AS also_ok;
sql
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 глав: внутреннее устройство, оптимизация, администрирование. Покупка разовая, доступ навсегда.