enum
В меню Brew у напитков появились размеры (S/M/L), у статей блога — теги, а маркетинг захотел складывать в заказ «произвольные опции» вроде «молоко овсяное, +1 шот». Три разные задачи — и три разных «контейнерных» типа в Postgres: enum для фиксированного набора размеров, массив для тегов, jsonb для гибких опций. Каждый удобен ровно в своей нише, и каждый легко применить не туда.
Цель юнита — познакомиться с тремя контейнерами и почувствовать, когда какой уместен. Это введение: глубокий разбор jsonb, GIN-индексов и полнотекстового поиска ждёт в модуле 07; здесь — базовые операторы и интуиция «когда нормализовать, а когда нет».
enum: упорядоченный конечный набор
enum — это тип с фиксированным списком значений (small, medium, large). Его сила не только в ограничении («сюда нельзя записать xl, такого значения нет»), но и в порядке: значения упорядочены по тому, как объявлены, а не по алфавиту. Поэтому 'small'::drink_size < 'large'::drink_size → true (small объявлен раньше), хотя по алфавиту large меньше. Это удобно для сортировки и сравнений по «шкале». Цена — негибкость: добавить значение можно (ALTER TYPE ... ADD VALUE), а удалить или переставить — больно.
Массивы: text[] и оператор @>
Массив (text[], int[], …) хранит список однотипных значений в одной колонке. В схеме Brew теги статьи лежат строкой 'coffee,basics' (так в kafka-cookbook — байт-совместимость), но string_to_array(tags, ',') разворачивает её в text[], а в Go это []string. Базовый оператор — @> («массив содержит»): tags @> ARRAY['coffee'] находит статьи с тегом coffee. На больших объёмах такой поиск ускоряет GIN-индекс (модуль 06/07). Массив хорош, когда значения простые, их немного и они не требуют собственных атрибутов; как только тегу нужны свои поля (цвет, счётчик) — пора в отдельную таблицу-связку.
jsonb: гибкость со звёздочкой
jsonb хранит структуру JSON в разобранном бинарном виде — с ним работают операторы -> (достать как jsonb), ->> (достать как text) и ? (есть ли ключ; в SQL это jsonb_exists). Ключевой нюанс на старте: ->> отдаёт значение как текст (oat), а -> оставляет его jsonb — со скобками-кавычками ("oat"). jsonb незаменим для действительно гибких, разреженных данных. Но это введение, и тут важнее предупреждение: jsonb — не повод не нормализовать. Поля, по которым ты фильтруешь, считаешь и джойнишь, почти всегда должны быть колонками; jsonb — для того, что по своей природе бесформенно. Подробности и грабли — в модуле 07.
Какой контейнер взять
| Контейнер | Что хранит | Доступ | Когда брать | Когда нормализовать |
|---|---|---|---|---|
enum | фиксированный упорядоченный набор | сравнение по шкале (<, >) | стабильные шкалы (S/M/L, статусы) | часто меняющийся справочник → таблица с FK |
массив (text[]) | список однотипных простых значений | @> «содержит» (ускорение GIN — 06/07) | простые теги/метки без своих атрибутов | тегу нужны свои поля → таблица-связка |
jsonb | разреженную/бесформенную структуру | ->, ->>, ? | по-настоящему гибкие, разреженные данные | фильтруешь / считаешь / джойнишь → колонка |
Правая колонка — это и есть граница: контейнер уместен, пока данные простые и «целиком»; как только по ним надо фильтровать, считать или джойнить по отдельным полям — пора в колонки и таблицы.
Что показывает наш код
Свой тип enum (в schema.sql) и три демонстрации. Порядок enum — на литералах; массивы и jsonb — на данных Brew и литералах:
SELECT ('small'::drink_size < 'large'::drink_size) AS small_lt_large, -- EnumOrder
('large'::drink_size < 'small'::drink_size) AS large_lt_small;
SELECT id, title, string_to_array(tags, ',')::text[] AS tag_list -- TagsAsArray
FROM articles ORDER BY id;
SELECT coalesce('{"size":"L","milk":"oat","shots":2}'::jsonb ->> 'milk', '') AS milk_text,
coalesce(('{"size":"L","milk":"oat","shots":2}'::jsonb -> 'milk')::text, '') AS milk_json,
jsonb_exists('{"size":"L","milk":"oat","shots":2}'::jsonb, 'milk') AS has_milk;tag_list sqlc типизирует как []string; результаты jsonb-операторов приезжают как строки. Как и 01-04, этот юнит добавляет свой объект в схему (тип drink_size), поэтому make db-reset накатывает его через brew.Apply (схема Brew → DDL юнита → seed).
Запуск
docker compose up -d
make lecture L=01-data-types/01-05-enums-arrays-and-jsonb-intro T=db-reset
make lecture L=01-data-types/01-05-enums-arrays-and-jsonb-introВывод:
1) enum drink_size = ('small','medium','large') — порядок по объявлению:
'small' < 'large' = true (по алфавиту было бы наоборот)
'large' < 'small' = false
2) string_to_array(tags) → text[] (в Go это []string):
ID ЗАГОЛОВОК TAGS ([]string)
1 Почему эспрессо — это база [coffee basics]
2 Гайд по колд брю [coffee cold-brew]
tags @> ARRAY['coffee'] → статей с тегом coffee: 2
3) jsonb '{"size":"L","milk":"oat","shots":2}' — базовые операторы:
->> 'milk' = oat (text: без кавычек)
-> 'milk' = "oat" (jsonb: с кавычками)
->> 'shots' = 2 (text '2')
? 'milk' = true (есть ли ключ)enum упорядочен по объявлению (small < large), а не по алфавиту. Теги развернулись в []string, и @> нашёл обе статьи с coffee. А jsonb показал главный контраст: ->> даёт чистый текст oat, -> — jsonb "oat" с кавычками.
Заборчик
Три контейнера — три соблазна:
enumтянет добавлять значения «на лету». В проде этоALTER TYPEпод миграцией, а удалить значение нельзя совсем; для часто меняющихся справочников лучше отдельная таблица с FK.- Массив манит сложить в него сущности со своими атрибутами. Тогда
@>-поиск и подсчёты превращаются в боль — нормализуй в таблицу-связку. jsonb— самый опасный. Он позволяет вообще не проектировать схему, и приложение быстро обрастает «полями внутри json», которые нельзя ни проверитьCHECK-ом, ни проиндексировать без ухищрений, ни заджойнить.
Правило простое: то, по чему фильтруешь / считаешь / джойнишь, — колонка; jsonb — только для по-настоящему бесформенного. Когда и как это делать правильно — модуль 07.
Что забрать с собой
enumупорядочен по объявлению значений, а не по алфавиту; хорош для фиксированных шкал, но негибок к изменениям.- Массивы (
text[]) + оператор@>(«содержит») удобны для простых списков; в схеме Brew теги — строка,string_to_arrayдаётtext[]→ Go[]string. jsonb:->>достаётtext,->—jsonb(с кавычками),jsonb_exists/?— наличие ключа. Это intro; глубина — модуль 07.- Контейнер — не замена нормализации: фильтруемое/считаемое/джойнимое держи колонками.
Дальше — модуль 02 «Схема, DDL и ограничения»: как из правильных типов собрать надёжную схему — IDENTITY против serial, NOT NULL, первичные и внешние ключи, UNIQUE/CHECK, генерируемые столбцы и мышление миграций.