enum

В меню Brew у напитков появились размеры (S/M/L), у статей блога — теги, а маркетинг захотел складывать в заказ «произвольные опции» вроде «молоко овсяное, +1 шот». Три разные задачи — и три разных «контейнерных» типа в Postgres: enum для фиксированного набора размеров, массив для тегов, jsonb для гибких опций. Каждый удобен ровно в своей нише, и каждый легко применить не туда.

Цель юнита — познакомиться с тремя контейнерами и почувствовать, когда какой уместен. Это введение: глубокий разбор jsonb, GIN-индексов и полнотекстового поиска ждёт в модуле 07; здесь — базовые операторы и интуиция «когда нормализовать, а когда нет».

enum: упорядоченный конечный набор

enum — это тип с фиксированным списком значений (small, medium, large). Его сила не только в ограничении («сюда нельзя записать xl, такого значения нет»), но и в порядке: значения упорядочены по тому, как объявлены, а не по алфавиту. Поэтому 'small'::drink_size < 'large'::drink_sizetrue (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разреженную/бесформенную структуру->, ->>, ?по-настоящему гибкие, разреженные данныефильтруешь / считаешь / джойнишь → колонка

Правая колонка — это и есть граница: контейнер уместен, пока данные простые и «целиком»; как только по ним надо фильтровать, считать или джойнить по отдельным полям — пора в колонки и таблицы.

Что показывает наш код

Свой тип enumschema.sql) и три демонстрации. Порядок enum — на литералах; массивы и jsonb — на данных Brew и литералах:

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

Запуск

sh
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

Вывод:

plaintext
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, генерируемые столбцы и мышление миграций.

·Модуль 02

Этот урок ещё впереди

Курс изучается по порядку — чтобы открыть этот шаг, сначала завершите предыдущие. Так контекст накапливается без пропусков.

/ вы пытались открыть
Типы данных / enum