Свойства relation: неупорядоченность, уникальность, атомарность
В прошлом уроке мы дали определение: relation — это множество tuples с одинаковым heading. Слово «множество» (set) — не украшение. Из него математически вытекает несколько свойств, и каждое из них имеет прямое практическое последствие при работе с SQL. Если их не понимать, вы будете писать запросы, которые «иногда работают» — а это худший вид кода в данных.
Разберём четыре свойства relation и две метрики его размера. Всё это — прямые следствия определения, а не отдельные правила, которые надо заучивать.
Свойство 1: строки неупорядочены
В relation нет порядка строк. Множество {1, 2, 3} и множество {3, 1, 2} — это одно и то же множество; точно так же relation не помнит, в каком порядке tuples были добавлены.
Практическое последствие: запрос SELECT без ORDER BY не даёт никаких гарантий порядка строк. Не «обычно возвращает в порядке вставки», не «как правило по первичному ключу» — никаких гарантий вообще. Сегодня СУБД вернёт строки в одном порядке, завтра — после обновления статистики, смены плана запроса или просто параллельного чтения — в другом.
-- Опасный код: порядок не гарантирован
SELECT name FROM users LIMIT 3;
-- Сегодня вернёт Ann, Bob, Cy
-- Завтра может вернуть Cy, Ann, Bob — план запроса изменился
-- Корректный код: порядок задан явно
SELECT name FROM users ORDER BY name LIMIT 3;
-- Всегда Ann, Bob, Cy
Если результат должен быть упорядочен — пишите ORDER BY. Всегда. Это не паранойя: это единственный способ заставить реляционную СУБД дать вам порядок, потому что в самой модели порядка не существует.
Свойство 2: столбцы неупорядочены
Аналогично строкам, столбцы relation тоже не имеют порядка. Attribute опознаётся по имени, а «третьего столбца» в реляционной модели не существует.
Практическое последствие: не полагайтесь на порядок столбцов. Две конкретные ошибки:
-- Плохо: SELECT * — порядок столбцов зависит от CREATE TABLE,
-- а кто-то может пересоздать таблицу с другим порядком
SELECT * FROM users;
-- Плохо: INSERT без списка столбцов — привязка по позиции
INSERT INTO users VALUES (42, '[email protected]', 29);
-- Если в таблицу добавят столбец, этот INSERT молча сломается
-- Хорошо: явные имена столбцов
INSERT INTO users (user_id, email, age) VALUES (42, '[email protected]', 29);
SELECT user_id, email FROM users;
SELECT * и INSERT ... VALUES без списка столбцов работают «по позиции» — а позиция в реляционной модели не определена. Явные имена столбцов делают код устойчивым к изменениям схемы.
Свойство 3: нет дублирующихся строк
Множество не содержит одинаковых элементов. Значит, в relation не может быть двух полностью идентичных tuples.
Из этого следует важнейший факт: в любом relation существует набор атрибутов, который уникально идентифицирует каждую строку. В худшем случае это все атрибуты сразу (раз строки различны, полный набор атрибутов их различает). На практике почти всегда есть меньший набор — это и есть ключ (следующий модуль курса целиком про ключи).
SQL, как мы видели, разрешает дубликаты, если у таблицы нет PRIMARY KEY/UNIQUE. Поэтому правило простое: объявляйте первичный ключ, и тогда таблица остаётся настоящим relation, к которому применима вся теория.
Свойство 4: каждое значение атомарно
Это самое тонкое свойство. Атомарность значит: значение в ячейке (на пересечении tuple и attribute) — одно и неделимое с точки зрения модели. В ячейке не может лежать список, массив, вложенная таблица или несколько значений «через запятую».
Это требование называется первой нормальной формой (1NF), и в модуле про нормализацию мы вернёмся к нему подробно. Сейчас важно понять, что нарушает атомарность.
НЕ атомарно (нарушение 1NF):
order_id | items
---------+----------------------------
1001 | 'apple, banana, cherry' <- три значения в одной ячейке
1002 | 'milk'
Чтобы посчитать товары в заказе 1001, СУБД пришлось бы
парсить строку. Реляционные операторы так не умеют.
Атомарно (1NF соблюдена):
order_id | item
---------+---------
1001 | apple
1001 | banana
1001 | cherry
1002 | milk
Теперь "сколько товаров в заказе 1001" — это COUNT(*),
а не разбор строки.
Почему атомарность критична? Реляционная алгебра — операторы select, project, join — работает со значениями как с чёрными ящиками: она их сравнивает на равенство, но не «заглядывает внутрь». Если в ячейке лежит 'apple, banana, cherry', то условие WHERE item = 'banana' его не найдёт — для СУБД это одна строка 'apple, banana, cherry', не равная 'banana'. Хранение списка через запятую — один из самых частых антипаттернов новичков, и он ломает ровно то, ради чего существует реляционная модель.
«Атомарно» не значит «нельзя разбить вообще никак». Дату 2026-05-20 можно разбить на год-месяц-день, и это нормально хранить как одно значение типа DATE. Атомарность — про то, что МОДЕЛЬ обращается со значением как с одним целым. Список из трёх товаров — это три независимых факта, и модель должна видеть три строки. Дата — один факт, одна ячейка.
Degree и cardinality: две метрики размера
У relation есть два числа, описывающих его размер. Их легко перепутать, поэтому запомните точно.
Degree (степень) — это число атрибутов relation, то есть «ширина» таблицы. Degree определяется heading-ом и не зависит от данных: пустая таблица с тремя столбцами имеет degree 3. Relation degree 1 называют унарным, degree 2 — бинарным, degree n — n-арным.
Cardinality (мощность) — это число tuples в relation, то есть «высота» таблицы. Cardinality определяется body и меняется при каждом INSERT/DELETE. Пустой relation имеет cardinality 0.
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
email TEXT,
age INTEGER
);
-- degree = 3 (три атрибута), cardinality = 0 (нет строк)
INSERT INTO users VALUES (1, '[email protected]', 20), (2, '[email protected]', 31);
-- degree = 3 (не изменился), cardinality = 2
-- cardinality считается так:
SELECT count(*) FROM users;
-- count
-- -----
-- 2
| Метрика | Что измеряет | Чем определяется | Меняется при INSERT? |
|---|---|---|---|
| Degree | число атрибутов (ширина) | heading (схема) | Нет |
| Cardinality | число tuples (высота) | body (данные) | Да |
Зачем это знать junior-инженеру? Слово cardinality встретится ещё много раз и в разных смыслах. В этом уроке cardinality — это число строк в таблице. В модуле про связи «кардинальность связи» (1:1, 1:N, M:N) — это про то, сколько строк одной таблицы соответствует строке другой. А ещё оптимизатор запросов оценивает «cardinality» промежуточных результатов, чтобы выбрать план. Контекст всегда подскажет, но базовое значение — «количество» — общее.
Почему эти свойства — не формальность
Может показаться, что четыре свойства relation — академическая теория, далёкая от ежедневной работы. На самом деле каждое из них — это конкретная привычка, отличающая инженера, чей код работает стабильно, от того, чей код «работает, пока не сломается».
Связь свойств с практикой прямая. «Нет порядка строк» — это привычка всегда писать ORDER BY, когда порядок важен, и никогда не полагаться на «обычно возвращается так». «Нет порядка столбцов» — это привычка писать явные имена столбцов в SELECT и INSERT, чтобы код пережил эволюцию схемы. «Нет дубликатов» — это привычка объявлять первичный ключ у каждой таблицы. «Атомарность» — это привычка не складывать списки в одну ячейку, а заводить отдельную строку на каждый факт.
Все четыре привычки объединяет одно: они защищают от недетерминированного поведения — кода, который даёт разный результат при одинаковых входных данных в зависимости от плана запроса, версии СУБД или порядка вставки. Недетерминированный код в данных особенно коварен: он проходит тесты, работает в разработке, а ломается в production через месяцы — когда таблица выросла, статистика обновилась и оптимизатор сменил план. Понимание свойств relation — это понимание того, на что в реляционной СУБД можно опираться, а на что нельзя.
ORDER BY и SELECT * — свойства relation в SQL-практикеХорошая проверка при написании запроса: спросите себя «а что гарантирует мне этот результат?». Если ответ опирается на порядок строк без ORDER BY, на порядок столбцов, на отсутствие дубликатов в таблице без ключа — значит, вы опираетесь на то, чего реляционная модель не обещает. Перепишите так, чтобы корректность следовала из явных конструкций, а не из везения.
Попробуй сам
Создайте в SQLite или PostgreSQL таблицу products с четырьмя столбцами и проделайте небольшой эксперимент:
- Сразу после
CREATE TABLE, до вставки данных, ответьте: чему равны degree и cardinality? Проверьте cardinality черезSELECT count(*). - Вставьте пять строк. Что изменилось — degree, cardinality, оба?
- Выполните
SELECT * FROM productsнесколько раз иSELECT * FROM products ORDER BY .... БезORDER BYпорядок может совпадать на маленькой таблице — но объясните себе, почему на это нельзя полагаться в production. - Попробуйте сознательно нарушить атомарность: добавьте столбец
tagsи впишите туда'sale, new, popular'. Затем напишите запрос «найди все продукты с тегом new» черезWHERE tags = 'new'. Он ничего не найдёт. Объясните почему и как правильно — через отдельную таблицуproduct_tagsсо строкой на каждый тег.