В каталоге товаров список тегов хранится в одном текстовом поле. Какое нарушение нормализации показывает такая схема?
CREATE TABLE product (
product_id INTEGER PRIMARY KEY,
name VARCHAR(200) NOT NULL,
tags VARCHAR(500) NOT NULL -- например: 'sql,backend,db'
);
Поле tags содержит несколько значений в одной строке, поэтому схема нарушает требование атомарности атрибутов, характерное для первой нормальной формы (1НФ). Корректнее представить связь «товар—тег» отдельными строками в таблице-связке.
Такая декомпозиция позволяет использовать внешние ключи, уникальность и обычные SQL-операции вместо разбора строки по разделителю.
Нормализация реляционных данных появилась как способ уменьшить избыточность и аномалии вставки, изменения и удаления. Одной из исходных проблем были повторяющиеся группы значений, когда в одном атрибуте фактически хранился список.
В классической реляционной модели значение атрибута рассматривается как одно значение из его домена. SQL допускает строковые, массивные и JSON-значения, но сама возможность хранить структуру внутри поля не делает CSV-список удобной реляционной моделью.
В примере сервер не видит отдельные теги как отдельные значения: для него sql,backend,db — одна строка. Поэтому нельзя надёжно задать внешний ключ на справочник тегов или обычное ограничение уникальности для каждой пары товара и тега.
Поиск по тегу превращается в разбор текста. Это создаёт ошибки на границах значений: например, поиск sql может ошибочно совпасть с частью nosql, а различия в пробелах и регистре могут породить дубликаты.
Изменение одного тега также требует переписывать весь список. Кроме того, база данных не может независимо контролировать существование каждого указанного тега и ссылочную целостность такой связи.
Обычно создают справочник тегов и таблицу связи «многие-ко-многим»:
Каждая строка product_tag означает одну связь. Составной первичный ключ запрещает повторить один и тот же тег у товара, внешние ключи запрещают ссылки на несуществующие товар или тег, а UNIQUE в tag не допускает повторения имени тега.
Преимущество — корректные соединения, индексация и проверяемые ограничения. Цена решения — дополнительные таблицы и JOIN; для чтения большого каталога может потребоваться продуманная индексация или отдельная проекция.
Массивы или JSON могут быть оправданы, если содержимое должно храниться как неделимый документ и не участвует в ссылочной целостности, фильтрации по отдельным элементам и независимом управлении. Но для реляционной связи с тегами таблица-связка обычно надёжнее.
В интернет-магазине сначала хранили теги CSV-строкой. Вариант с регулярными выражениями и функциями разбиения почти не менял схему, но усложнял запросы, зависел от формата строки и не позволял гарантировать отсутствие ссылок на удалённые теги.
Вариант с массивом был проще для записи и мог быть эффективен для отдельных сценариев чтения, однако контроль уникальности, справочника и связей становился зависимым от конкретной СУБД и её операторов.
Выбрали таблицы tag и product_tag: запись связи стала обычной вставкой, удаление тега — контролируемой операцией с внешним ключом, а поиск товаров по тегу — индексируемым соединением. После миграции стало возможно независимо переименовывать, объединять и удалять теги.
1. Является ли любое JSON-поле нарушением 1НФ?
Нет, такой вывод слишком категоричен. Нужно учитывать модель данных и назначение поля: если JSON хранится как неделимый документ, его использование может быть осознанным решением. Но если из него регулярно извлекают элементы, устанавливают связи или проверяют ограничения на отдельные значения, реляционная таблица часто лучше выражает структуру и обеспечивает целостность.
2. Почему отдельная таблица лучше даже при наличии функции разбиения строк?
Функция разбиения помогает получить строки во время запроса, но не превращает исходные данные в нормализованные записи. Она не заменяет первичный ключ, внешние ключи и уникальное ограничение для каждой связи. Поэтому целостность и оптимизация остаются сложнее, чем при хранении связи отдельными строками.
3. Какой индекс обычно нужен для поиска товаров по тегу?
Первичный ключ (product_id, tag_id) эффективно поддерживает поиск связей по product_id, но не обязательно оптимален для поиска всех товаров по tag_id, поскольку tag_id не является первым столбцом индекса. Для обратного направления обычно добавляют индекс, например CREATE INDEX ON product_tag (tag_id, product_id). Это не меняет нормализацию, а учитывает два разных шаблона доступа.