Learning Platform
Глоссарий Troubleshooting
Урок 05.05 · 21 мин
Средний
type-systemunionenum

UNION и ENUM: теговый тип и dictionary-семантика

Два типа из этого урока решают противоположные задачи. UNION нужен, когда колонка должна вмещать значения разных типов — то «целое», то «строку». ENUM нужен, когда колонка, наоборот, ограничена очень узким набором допустимых строковых значений — статус заказа из четырёх вариантов, день недели из семи. Объединяет их то, что оба часто понимают поверхностно, а у обоих есть важная внутренняя механика.

Главная мысль урока — про ENUM. На уровне SQL ENUM выглядит как «строка из ограниченного набора», и легко решить, что это просто VARCHAR с проверкой. Но на уровне хранения ENUM — это словарь (dictionary): сами строки хранятся один раз в метаданных типа, а в колонке лежат компактные целочисленные ссылки. Это даёт ENUM и компактность, и скорость, которых у VARCHAR нет.


UNION: значение одного из нескольких типов

В строго типизированной СУБД у колонки один тип. Но иногда данные по своей природе разнородны: поле «значение измерения» в одних строках число, в других — текстовая метка вроде 'N/A'; поле «идентификатор» то числовое, то строковое. UNION — теговый тип (tagged union), который позволяет колонке хранить значение одного из заранее перечисленных типов.

UNION объявляется как набор именованных вариантов (members), каждый со своим типом:

-- Колонка result может быть либо числом, либо текстом, либо булевым
CREATE TABLE measurements (
    id INTEGER,
    result UNION(num DOUBLE, label VARCHAR, flag BOOLEAN)
);

INSERT INTO measurements VALUES
    (1, 42.5),        -- сохранится в варианте num
    (2, 'pending'),   -- сохранится в варианте label
    (3, true);        -- сохранится в варианте flag

Слово «теговый» здесь буквально. Каждое значение UNION несёт не только данные, но и тег — какой именно вариант сейчас активен. Значение 42.5 хранится с тегом num, 'pending' — с тегом label. Тег — это часть значения, и по нему всегда известно, как данные интерпретировать.

Узнать активный вариант и достать значение конкретного варианта можно функциями union_tag и union_extract, а также точечной записью:

-- union_tag — какой вариант активен; точка — значение варианта
SELECT
    id,
    union_tag(result)    AS active_member,
    result.num           AS as_num,    -- NULL, если активен не num
    result.label         AS as_label
FROM measurements;

result.num достаёт значение, только если активен вариант num; иначе вернёт NULL. Это безопасно: вы спрашиваете «дай мне число, если оно тут число».

UNION: значение несёт тег активного варианта
Значение 42.5Числовое значение, сохранённое в колонку UNION(num, label, flag).
хранится с тегом
тег = numТег — часть значения. По нему движок знает, что активен вариант num и данные надо читать как DOUBLE.
union_tag / точка
result.num вернёт 42.5Доступ к активному варианту даёт значение; доступ к неактивному варианту вернёт NULL.
NOTE

UNION отличается от простого хранения всего как VARCHAR или как JSON тем, что каждый вариант — это полноценный типизированный слот. Число внутри UNION хранится как настоящий DOUBLE, со всеми его свойствами и сжатием, а не как текст. UNION сохраняет типизацию там, где разнородность данных неизбежна, вместо того чтобы стирать её приведением всего к строке.

UNION — относительно редкий тип. Чаще всего разнородность данных стоит устранять на этапе моделирования: разнести разные типы значений по разным колонкам или нормализовать данные. UNION уместен, когда разнородность по-настоящему неустранима и присуща домену.


ENUM: ограниченный набор строк

ENUM — противоположная задача. Это тип для колонки, чьи значения берутся из небольшого, заранее известного и фиксированного множества строк. Классические примеры: статус заказа ('new', 'paid', 'shipped', 'cancelled'), приоритет ('low', 'medium', 'high'), день недели.

ENUM создаётся как именованный тип с перечислением допустимых значений:

-- Объявляем тип-перечисление
CREATE TYPE order_status AS ENUM ('new', 'paid', 'shipped', 'cancelled');

-- Используем его как тип колонки
CREATE TABLE orders (
    id INTEGER,
    status order_status
);

INSERT INTO orders VALUES (1, 'paid'), (2, 'new'), (3, 'shipped');

На уровне SQL ENUM-колонка ведёт себя как строковая: сравнивается со строковыми литералами, выводится как текст. Но есть жёсткое ограничение — попытка вставить значение вне набора отвергается:

-- Значение вне набора — ошибка на этапе вставки
INSERT INTO orders VALUES (4, 'refunded');
-- ОШИБКА: 'refunded' не входит в ENUM order_status

Это первая польза ENUM — валидация на уровне типа. Колонка физически не может содержать значение вне допустимого набора. Опечатка 'payed' вместо 'paid' будет отклонена базой, а не просочится в данные, как с обычным VARCHAR.


ENUM на уровне хранения: это словарь

Вот ключевая часть урока. ENUM — это не «VARCHAR с проверкой». На уровне хранения ENUM устроен как dictionary, и это меняет всё.

Когда вы создаёте CREATE TYPE order_status AS ENUM (...), DuckDB строит словарь: каждой строке набора назначается небольшое целое число — позиция в перечислении. 'new' получает 0, 'paid' — 1, 'shipped' — 2, 'cancelled' — 3. Сами строки 'new', 'paid' и так далее хранятся ровно один раз — в метаданных типа.

В самой колонке status строки не хранятся вообще. Там лежат только целочисленные ссылки в словарь. Колонка из миллиона заказов со статусами — это миллион маленьких целых, а не миллион строк.

ENUM на уровне хранения — это словарь
Метаданные типа: словарьСтроки набора хранятся ровно один раз в определении типа. Каждой назначен номер.
колонка ссылается на словарь
Колонка status: [1, 0, 2, 1, 1, 2, ...]В самой колонке лежат не строки, а компактные целые числа — ссылки в словарь. Миллион строк = миллион маленьких целых.
при выводе
число -> строка по словарюЧеловекочитаемая строка собирается из словаря только в момент вывода результата.

Из dictionary-устройства следуют три практических преимущества ENUM перед VARCHAR.

Первое — компактность. Целое число-ссылка занимает 1-2 байта (ширина зависит от размера набора), а строка 'cancelled' — девять байт плюс накладные расходы. Колонка статусов в ENUM в разы меньше той же колонки в VARCHAR. Это меньше места на диске и, что важнее для аналитики, меньше данных читать с диска при сканировании.

Второе — скорость сравнения и группировки. Сравнить два ENUM-значения — это сравнить два целых числа, самую быструю операцию. GROUP BY status по ENUM-колонке группирует целые, а не строки. Это заметно быстрее, чем работа со строковой колонкой, где сравнение строк дороже.

Третье — валидация бесплатно. Проверка «значение в наборе» — это естественное следствие словаря: если строки нет в словаре, для неё просто нет номера. Валидация не требует отдельного механизма.

АспектENUMVARCHAR
Что лежит в колонкеЦелочисленные ссылки (1-2 байта)Сами строки
Размер на дискеМаленькийБольше, зависит от длины строк
Сравнение и GROUP BYСравнение целых — быстроСравнение строк — дороже
Значение вне набораОтвергаетсяПринимается любое
Гибкость набораФиксирован, расширяется через ALTER TYPEЛюбое значение
TIP

Dictionary-семантика ENUM — родственник схемы сжатия dictionary encoding, которой DuckDB сжимает обычные VARCHAR-колонки с повторами (это разбирается в модуле про компрессию). Разница в том, что для VARCHAR словарь строится автоматически на лету при сжатии сегмента, а для ENUM словарь задан явно и заранее, на уровне типа. ENUM — это, по сути, «dictionary encoding, зафиксированное в схеме».


Когда применять ENUM

ENUM оправдан, когда выполнены оба условия: множество значений мало и оно стабильно. «Мало» — десятки значений, не тысячи. «Стабильно» — набор меняется редко, на уровне изменения схемы, а не данных.

Хорошие кандидаты: статусы (заказа, платежа, задачи), категории фиксированного классификатора, дни недели, уровни (приоритет, severity лога), коды стран, если их множество в системе ограничено. Эти колонки одновременно низкокардинальны (мало уникальных значений) и часто участвуют в WHERE и GROUP BY — именно там dictionary-устройство даёт выигрыш.

Плохие кандидаты для ENUM: имена пользователей, email, произвольный текст, идентификаторы — всё, где значений много или они постоянно новые. Если набор расширяется с каждой строкой данных, ENUM не подходит: добавление значения в ENUM — это ALTER TYPE, операция уровня схемы, и делать её на каждую новую строку нельзя.

WARNING

Расширение ENUM новым значением выполняется через ALTER TYPE ... ADD VALUE — это изменение определения типа, а не данных. Если домен колонки растёт непредсказуемо и часто, постоянные ALTER TYPE становятся обузой, и здесь VARCHAR (возможно, с проверкой через CHECK или со словарным сжатием, которое DuckDB применит сам) удобнее. ENUM — для действительно стабильных наборов.

Итог урока: UNION и ENUM — про противоположные ситуации. UNION принимает разнородность, когда она неустранима, сохраняя типизацию каждого варианта через тег. ENUM сужает колонку до фиксированного набора строк и за счёт dictionary-устройства делает её компактной и быстрой. Понимать, что ENUM — это словарь с целыми ссылками, а не «VARCHAR с галочкой», важно: именно из этого следуют все его преимущества.


Попробуй сам

Запустите DuckDB CLI:

CREATE TYPE order_status AS ENUM ('new', 'paid', 'shipped', 'cancelled');
CREATE TABLE orders (id INTEGER, status order_status);
INSERT INTO orders VALUES (1, 'paid'), (2, 'new'), (3, 'shipped'), (4, 'paid');

Задания:

  1. Сгруппируйте заказы по status со счётчиком. Запрос работает со строками или с целыми числами внутри? Объясните.
  2. Попробуйте INSERT INTO orders VALUES (5, 'returned'). Посмотрите на ошибку и объясните, как dictionary-устройство ENUM делает эту проверку естественной.
  3. Расширьте набор: ALTER TYPE order_status ADD VALUE 'returned'. Теперь повторите вставку из задания 2. Объясните, почему это операция уровня схемы.
  4. Создайте таблицу с колонкой типа UNION(n INTEGER, s VARCHAR). Вставьте в неё одну строку с числом и одну со строкой. Через union_tag выведите, какой вариант активен в каждой строке.
  5. Объясните своими словами, почему колонка статусов в ENUM занимает на диске меньше места, чем та же колонка в VARCHAR.
Dictionary encoding: физическая основа ENUM
Проверка знанийKnowledge check
Почему ENUM на уровне хранения — это не «VARCHAR с проверкой», и какие практические преимущества даёт его dictionary-устройство?
ОтветAnswer
ENUM на уровне SQL ведёт себя как строка из ограниченного набора, но на уровне хранения он устроен как словарь (dictionary). При создании типа CREATE TYPE ... AS ENUM (...) DuckDB строит словарь: каждой строке набора назначается небольшое целое — её позиция в перечислении (new=0, paid=1, shipped=2, cancelled=3). Сами строки хранятся ровно один раз, в метаданных типа. В самой колонке строки не лежат вообще — там только компактные целочисленные ссылки в словарь: миллион заказов со статусами это миллион маленьких целых, а не миллион строк. Человекочитаемая строка собирается из словаря только при выводе. Из этого следуют три преимущества перед VARCHAR. Первое — компактность: целое-ссылка занимает 1-2 байта, а строка вроде 'cancelled' — девять байт плюс накладные расходы; колонка ENUM в разы меньше на диске, и при сканировании читается меньше данных. Второе — скорость: сравнить два ENUM-значения это сравнить два целых, самую быструю операцию процессора, и GROUP BY по ENUM группирует целые, а не строки — заметно быстрее. Третье — валидация бесплатно: проверка 'значение в наборе' это естественное следствие словаря (нет строки в словаре — нет номера), отдельный механизм не нужен, и значение вне набора физически не может попасть в колонку. ENUM по сути — это dictionary encoding, зафиксированное в схеме типа, в отличие от автоматического словарного сжатия VARCHAR, которое строится на лету. Применять ENUM стоит для малых и стабильных наборов (статусы, категории, уровни); для растущих доменов постоянные ALTER TYPE становятся обузой.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Как ENUM устроен на уровне хранения?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6