Признайтесь, у каждого был соблазн сделать это. У вас есть сущность (например, User), и нужно сохранить список их ролей или IDs купленных товаров. Создавать отдельную таблицу кажется оверхедом, и вы решаете: "А, запишу просто строкой через запятую".
В БД это выглядит так:
role_ids: "1,4,12"
Почему это бомба замедленного действия и как это лечить? Давайте разбираться.
Почему это плохо (The Pain):
1. Сложный поиск. Найти всех пользователей с role_id = 1 через LIKE '%1%' - это больно. Вы найдете и 1, и 12, и 100. Придется писать монструозные регулярки.
2. Никаких индексов. База данных не может эффективно индексировать подстроки в таком формате. Full scan обеспечен.
3. Проблемы с JOIN. Вы не сможете сделать нормальный JOIN с таблицей ролей.
4. Целостность данных. Вы не можете повесить Foreign Key. Никто не помешает записать туда "1, 4, apple, NULL".
5. Атомарность обновлений. Удалить роль 4 из строки "1,4,12" - это чтение, парсинг на бекенде и перезапись. Состояние гонки (race condition) гарантировано.
Как делать правильно:
Вариант 1: Классическая нормализация (Junction Table)
Создайте связующую таблицу. Это золотой стандарт для реляционных БД (PostgreSQL, MySQL, Oracle).
Теперь выборка всех админов - это моментальный запрос с использованием индексов.
Вариант 2: Массивы или JSONB (PostgreSQL)
Если вы используете PostgreSQL и вам действительно не нужны жесткие FK (Foreign Keys) на каждый элемент, можно использовать нативные типы.
Никогда не храните списки в VARCHAR, если вам когда-либо придется искать по содержимому этого списка или джойнить его. Экономия 5 минут на старте обернется часами рефакторинга позже.
В БД это выглядит так:
role_ids: "1,4,12"
Почему это бомба замедленного действия и как это лечить? Давайте разбираться.
Почему это плохо (The Pain):
1. Сложный поиск. Найти всех пользователей с role_id = 1 через LIKE '%1%' - это больно. Вы найдете и 1, и 12, и 100. Придется писать монструозные регулярки.
2. Никаких индексов. База данных не может эффективно индексировать подстроки в таком формате. Full scan обеспечен.
3. Проблемы с JOIN. Вы не сможете сделать нормальный JOIN с таблицей ролей.
4. Целостность данных. Вы не можете повесить Foreign Key. Никто не помешает записать туда "1, 4, apple, NULL".
5. Атомарность обновлений. Удалить роль 4 из строки "1,4,12" - это чтение, парсинг на бекенде и перезапись. Состояние гонки (race condition) гарантировано.
Как делать правильно:
Вариант 1: Классическая нормализация (Junction Table)
Создайте связующую таблицу. Это золотой стандарт для реляционных БД (PostgreSQL, MySQL, Oracle).
SQL:
-- Плохо ❌
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
role_ids VARCHAR(255) -- "1,2"
);
-- Хорошо ✅
CREATE TABLE user_roles (
user_id INT,
role_id INT,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
Вариант 2: Массивы или JSONB (PostgreSQL)
Если вы используете PostgreSQL и вам действительно не нужны жесткие FK (Foreign Keys) на каждый элемент, можно использовать нативные типы.
SQL:
-- Допустимо в Postgres ✅
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT,
role_ids INT[] -- массив целых чисел
);
-- Поиск (очень быстрый с GIN индексом):
SELECT * FROM users WHERE 1 = ANY(role_ids);