Чтобы облегчить считываемость информации в таблице, программисты приводят данные, которые представлены в реляционной модели, к нормальной форме. В этом уроке мы узнаем, что это за форма, а также разберем ее первый уровень.
Нормальная форма
Возьмем для примера интернет-магазин, в котором продается электроника. Когда пользователь делает заказ, в базу данных заносится запись об этом. В нее входит вся необходимая информация: данные пользователя, какой товар он купил, сколько он стоил и адрес доставки. Затем эти данные используются всеми подразделениями интернет-магазина — от бухгалтеров до службы доставки.
Таблица order_items
first_name | last_name | address | item | price |
---|---|---|---|---|
Сергей | Иванов | Москва, ул. Промышленная | утюг | 15.00 |
Иван | Петров | Самара, ул. Энгельса | кофеварка | 5000.00 |
Виктор | Сидоров | Омск, ул. Дворцовая | утюг, телевизор | 1000.00, 6500.00 |
Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
В первой строке последнего столбца цена указана в долларах, в остальных записях — это рубли. Последняя запись повторяет предыдущую, потому что этот заказ выполнил тот же человек, но сделал это в другое время.
В этой табличке много повторяющейся информации. Приведем ее к правильной структуре с точки зрения реляционной модели. Для этого приведем данные к нормальной форме — это требования, которые минимизируют избыточность данных, потенциально приводящих к логическим ошибкам.
Всего существует шесть нормальных форм, которые включают определенные требования. С каждым следующим уровнем требования все жестче, так как включают в себя предыдущие уровни.
В рамках курса мы разберем три нормальные формы. В этом уроке познакомимся с первой.
Первая нормальная форма
Первая нормальная форма сводится к трем правилам:
- Каждая ячейка таблицы может хранить только одно значение
- Все данные в одной колонке могут быть только одного типа
- Каждая запись в таблице должна однозначно отличаться от других записей
Разберем каждое правило подробнее.
Каждая ячейка – одно значение
Вернемся к примеру выше. У одной записи поля item и price содержат два значения через запятую. У такого способа организации данных много недостатков. Например, пропадает возможность делать обычную выборку по условиям:
-- Как задать условие, чтобы получить все записи о проданных утюгах?
SELECT * FROM order_items WHERE item = ...;
Другая проблема связана с типами данных. Поле price в таблице order_items имеет числовой тип (numeric). Если мы захотим хранить там более одного значения, то тип превратится в строковый, а все данные станут обычными строками.
При такой организации невозможно проверить корректность данных и формат числа. Становится проблематично выполнить агрегирующие запросы, например, считать выручку за определенный месяц одним запросом.
Чтобы избавиться от перечислений в ячейках, можно создать новые записи:
first_name | last_name | address | item | price |
---|---|---|---|---|
Сергей | Иванов | Москва, ул. Промышленная | утюг | 15.00 |
Иван | Петров | Самара, ул. Энгельса | кофеварка | 5000.00 |
Виктор | Сидоров | Омск, ул. Дворцовая | утюг | 1000.00 |
Виктор | Сидоров | Омск, ул. Дворцовая | телевизор | 6500.00 |
Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
Теперь на одной строке находится информация только по одному товару. Так мы избавились от перечислений в поле, что позволит выполнять агрегирующие запросы, а также не будет путаницы с типами данных.
Данные одного типа
Снова вернемся к таблице. Верхняя запись в ней содержит цену в долларах, хотя все остальные цены указаны в рублях. Технически база никак не укажет на это. И доллары, и рубли представлены числами, но с точки зрения программы у этих чисел разная природа.
Разные данные в рамках одного поля тоже не дают выполнить агрегирующие запросы, например, поиск сумм, максимального, минимального. Еще усложняется обработка данных на уровне кода. В коде придется каким-то образом понимать, что из себя представляют данные.
Вот еще несколько примеров с похожей ситуацией:
- Хранение даты свадьбы в поле «день рождения»
- Хранение номера телефона вместо адреса в поле «адрес»
Исправленная версия таблицы:
first_name | last_name | address | item | price |
---|---|---|---|---|
Сергей | Иванов | Москва, ул. Промышленная | утюг | 1000.00 |
Иван | Петров | Самара, ул. Энгельса | кофеварка | 5000.00 |
Виктор | Сидоров | Омск, ул. Дворцовая | утюг | 1000.00 |
Виктор | Сидоров | Омск, ул. Дворцовая | телевизор | 6500.00 |
Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
Мы сконвертировали цену утюга в первой строке из долларов в рубли. Теперь у данных в поле price один тип. Так программе будет легче выполнять агрегирующие запросы.
Уникальные записи
Последние две записи в таблице выглядят идентично, хотя это два разных заказа. Их сделал один человек, но в разное время:
first_name | last_name | address | item | price |
---|---|---|---|---|
Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
Реляционная модель требует от нас уникальности каждой записи. Иначе нельзя понять, что к чему относится и с какой записью нужно работать при изменениях. Можно начать править не то и потерять важную информацию. При этом мы не можем полагаться на порядок данных внутри таблицы, так как он не гарантирован.
Реализовать уникальность можно несколькими способами, например, добавить новое поле с датой заказа, которое сделает запись уникальной. Этот способ не очень надежный и не очень удобный в работе. Придется постоянно анализировать весь набор полей.
Лучше добавить первичный ключ (PRIMARY KEY) — поле или набор полей, которые содержат уникальное значение для каждой записи. Первичный ключ не может меняться, его значение однозначно определяет любую запись в таблице.
Разберем два вида первичного ключа:
- Естественный — когда используются значения из окружающего мира, например, email, ФИО или паспортные данные. При этом нужно убедиться, что ключ не будет повторяться. Такие первичные ключи используют редко из-за их ненадежности. Часто они не уникальны и могут изменяться или повторяться. Например, номер паспорта меняется при смене документа
- Суррогатный — когда используются автоматически генерируемые уникальные значения. Такой ключ поддерживается любой базой данных «из коробки». Иногда это просто числа, а иногда — сложные число-буквенные строки или хеши
Добавим в нашу таблицу первичный ключ:
id | first_name | last_name | address | item | price |
---|---|---|---|---|---|
8 | Сергей | Иванов | Москва, ул. Промышленная | утюг | 1000.00 |
2 | Иван | Петров | Самара, ул. Энгельса | кофеварка | 5000.00 |
7 | Виктор | Сидоров | Омск, ул. Дворцовая | утюг | 1000.00 |
4 | Виктор | Сидоров | Омск, ул. Дворцовая | телевизор | 6500.00 |
9 | Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
6 | Сергей | Иванов | Москва, ул. Матросова | ноутбук | 20000.00 |
Первичный ключ принято создавать первым полем с названием id. Для первичного ключа обязательно указывать PRIMARY KEY в описании таблицы:
-- Первичный ключ только один на таблицу
CREATE TABLE products (
id bigint PRIMARY KEY,
first_name varchar(255),
last_name varchar(255),
address varchar(255),
item varchar(255),
price numeric -- специальный тип данных, который подходит под работу с деньгами. Обеспечивает высокую точность при расчетах.
);
Такой ключ все еще нужно формировать самостоятельно, но теперь база данных сама следит за уникальностью. При попытке создать запись с повторяющимися первичными ключами возникнет ошибка.
Выводы
В этом уроке мы узнали, что такое нормальная форма в реляционной модели. Она помогает облегчить считываемость информации в таблице. Еще мы подробно разобрали первый уровень формы. Так мы узнали, что каждая ячейка таблицы может хранить только одно значение. Иначе может произойти путаница с типами данных, из-за чего будет невозможно проверить их корректность.
Также все данные в одной колонке могут быть только одного типа. Данные разного типа усложняют работу с таблицей, так как в коде придется каким-то образом понимать, что из себя представляют данные. А чтобы отличать записи друг от друга, нужно использовать суррогатный первичный ключ, который сделает каждую запись уникальной.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»