Пошук уроків, статей та іншого контенту
Організуйте таблиці так, щоб кожне поле містило атомарне значення, а рядки були однозначно ідентифіковані.
Перша нормальна форма, або 1NF, встановлює базові правила організації таблиць:
кожне поле містить одне атомарне значення;
у таблиці немає списків значень в одному полі;
немає повторюваних груп стовпців;
кожен рядок можна однозначно ідентифікувати.
Атомарне значення — це значення, яке розглядається як одне ціле в межах цієї таблиці. Наприклад, номер телефону може бути одним значенням, а список номерів телефонів через кому — уже кількома значеннями в одному полі.
Розглянемо таблицю замовлень:
orders
------------------------------------------------
id | customer_name | products
------------------------------------------------
1 | Олена | Книга, Ручка
2 | Андрій | ЗошитПоле products містить список товарів. Це створює проблеми:
складно знайти всі замовлення, у яких є конкретний товар;
складно порахувати кількість кожного товару;
складно змінити один товар у списку;
складно зберігати додаткові дані про товар у замовленні, наприклад кількість або ціну.
Інший приклад порушення:
customer_id | phone_1 | phone_2 | phone_3
-------------+---------------+---------------+---------------
1 | +380501112233 | +380671112233 | NULLСтовпці phone_1, phone_2, phone_3 є повторюваною групою. Якщо користувачеві знадобиться четвертий номер, доведеться змінювати структуру таблиці.
Замість списку товарів у полі потрібно створити окремий рядок для кожного товару:
order_items
-----------------------------------------
order_id | product_name | quantity
-----------------------------------------
1 | Книга | 1
1 | Ручка | 2
2 | Зошит | 3Тепер кожна клітинка містить одне значення:
order_id — ідентифікатор замовлення;
product_name — один товар;
quantity — одна кількість.
Одне замовлення може мати багато рядків у order_items, але кожен рядок описує одну позицію замовлення.
-- Видаляємо таблиці, якщо вони вже існують.
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_name text NOT NULL,
created_at date NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE order_items (
order_id integer NOT NULL REFERENCES orders(id),
product_name text NOT NULL,
quantity integer NOT NULL CHECK (quantity > 0),
-- Один товар може зустрічатися в одному замовленні лише один раз.
PRIMARY KEY (order_id, product_name)
);
INSERT INTO orders (customer_name)
VALUES
('Олена'),
('Андрій');
INSERT INTO order_items (order_id, product_name, quantity)
VALUES
(1, 'Книга', 1),
(1, 'Ручка', 2),
(2, 'Зошит', 3);
SELECT
orders.id AS order_id,
orders.customer_name,
order_items.product_name,
order_items.quantity
FROM orders
JOIN order_items ON order_items.order_id = orders.id
ORDER BY orders.id, order_items.product_name;Результат запиту міститиме окремий рядок для кожної позиції:
order_id | customer_name | product_name | quantity
----------+---------------+--------------+----------
1 | Олена | Книга | 1
1 | Олена | Ручка | 2
2 | Андрій | Зошит | 3Така структура відповідає 1NF:
у product_name зберігається один товар;
у quantity зберігається одне число;
немає стовпців product_1, product_2, product_3;
кожен рядок має однозначний ідентифікатор через складений первинний ключ (order_id, product_name).
У таблиці не повинно бути двох невідрізнюваних рядків. Для цього використовують первинний ключ.
Наприклад:
CREATE TABLE users (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
full_name text NOT NULL
);У цій таблиці:
id однозначно ідентифікує кожного користувача;
email також не може повторюватися завдяки обмеженню UNIQUE;
два користувачі не можуть мати однаковий id або email.
Первинний ключ не обов’язково має складатися лише з одного стовпця. У таблиці позицій замовлення природним ключем може бути комбінація:
PRIMARY KEY (order_id, product_name)Це означає, що пара значень order_id і product_name має бути унікальною.
Окремі рядки замість списків спрощують запити.
Знайти всі замовлення з товаром Ручка:
SELECT
orders.id,
orders.customer_name,
order_items.quantity
FROM orders
JOIN order_items ON order_items.order_id = orders.id
WHERE order_items.product_name = 'Ручка';Порахувати загальну кількість кожного товару:
SELECT
product_name,
SUM(quantity) AS total_quantity
FROM order_items
GROUP BY product_name
ORDER BY product_name;Такі запити були б значно складнішими, якби товари зберігалися в одному текстовому полі через кому.
Атомарність визначається тим, як застосунок використовує значення.
Наприклад, повне ім’я можна зберігати в одному полі:
full_name textЯкщо застосунку потрібно окремо шукати ім’я та прізвище, краще використати два поля:
first_name text,
last_name textАдреса також може бути одним значенням для простого застосунку. Але якщо потрібно окремо фільтрувати за містом, вулицею або поштовим індексом, ці частини варто зберігати в окремих стовпцях.
Важливо не розділяти кожен текст механічно. Потрібно враховувати, які операції виконуватимуться над даними.
У PostgreSQL існує тип масиву, наприклад:
tags text[]Для початкового проєктування таблиць правило 1NF зазвичай формулюють так: не зберігайте набір окремих сутностей у одному полі.
Якщо з кожним елементом списку потрібно працювати окремо — шукати, фільтрувати, пов’язувати його з іншими таблицями або зберігати додаткові властивості — краще створити окремі рядки в дочірній таблиці.
Наприклад, замість списку товарів у замовленні використовуйте таблицю order_items.
phones = '+380501112233, +380671112233'Краще створити окрему таблицю номерів:
CREATE TABLE customer_phones (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id integer NOT NULL,
phone text NOT NULL
);skill_1 | skill_2 | skill_3Кількість навичок може змінюватися, тому повторювані стовпці не масштабуються. Краще зберігати кожну навичку в окремому рядку дочірньої таблиці.
Таблиця без первинного ключа може містити дублікати, які складно відрізнити та змінити.
CREATE TABLE products (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);JSON може бути корисним для спеціальних випадків, але не варто використовувати його лише для того, щоб заховати звичайні структуровані дані в одному полі. Якщо дані потрібно регулярно фільтрувати або пов’язувати, реляційна структура з окремими рядками зазвичай зрозуміліша.
Поставте до таблиці такі запитання:
Чи містить кожна клітинка одне значення?
Чи є поля зі списками через кому?
Чи є стовпці на кшталт item_1, item_2, item_3?
Чи можна однозначно ідентифікувати кожен рядок?
Чи потрібно збільшувати кількість стовпців, якщо з’являється ще один елемент?
Чи можна окремо знайти та змінити кожен елемент списку?
Якщо відповідь на одне з цих питань вказує на проблему, структуру таблиці варто переглянути.
1NF вимагає атомарних значень у клітинках.
Не слід зберігати списки значень через кому в одному полі.
Повторювані стовпці на кшталт phone_1, phone_2 порушують зручну реляційну структуру.
Для набору пов’язаних елементів потрібно створювати окрему таблицю з одним рядком на елемент.
Кожен рядок повинен мати первинний ключ або інший спосіб однозначної ідентифікації.
Правильна 1NF спрощує пошук, фільтрацію, агрегацію та оновлення даних.