Реляционная база данных хранит информацию в таблицах: каждая строка — одна запись (клиент, заказ), каждый столбец — одно свойство с заданным типом. Первичный ключ однозначно идентифицирует строку, а внешний ключ связывает таблицы: заказ ссылается на клиента, и база не даст создать заказ несуществующему клиенту. СУБД, например PostgreSQL, проверяет эти правила сама, выдерживает одновременную работу многих пользователей и отвечает на вопросы на языке SQL — этого не умеет файл Excel.
После материала вы сможете
- описать таблицу, строку и столбец на примере клиентов и заказов интернет-магазина
- объяснить, зачем нужен первичный ключ и почему телефон или имя для него не подходят
- показать на схеме, как внешний ключ связывает заказ с клиентом и что база сделает при попытке нарушить связь
- перечислить, что СУБД даёт по сравнению с файлом Excel: ограничения, одновременный доступ, транзакции и запросы
Что знать заранее
Проверено на
PostgreSQL 17 (17.11, образ postgres:17-alpine)
Сверено с документацией: 11 октября 2026 г.
Содержание
Данные как таблицы: строки и столбцы
Представьте небольшой интернет-магазин кофе и чая. Ему нужно помнить клиентов, товары, заказы и то, что лежит в каждом заказе. Реляционная база данных раскладывает это по таблицам. Документация PostgreSQL определяет таблицу как именованный набор строк: у всех строк одной таблицы одинаковый набор именованных столбцов, и у каждого столбца свой тип данных.
Строка — одна запись: один клиент или один заказ. Столбец — одно свойство всех записей: имя, город, e-mail, дата регистрации. В таблице клиентов ниже восемь строк и пять столбцов. Значение NULL означает «значение неизвестно или отсутствует»: у клиента не указан e-mail или город. Это не пустая строка и не ноль — к этому различию мы ещё вернёмся в уроках по SQL.
1
- name
- Aigerim Seitova
- city
- Almaty
- aigerim@example.com
- created_on
- 2026-06-02
2
- name
- Daniyar Akhmetov
- city
- Astana
- NULL
- created_on
- 2026-06-15
3
- name
- Madina Omarova
- city
- Almaty
- madina@example.com
- created_on
- 2026-07-01
4
- name
- Timur Kim
- city
- Shymkent
- timur@example.com
- created_on
- 2026-07-20
5
- name
- Aliya Bekova
- city
- NULL
- aliya@example.com
- created_on
- 2026-08-05
6
- name
- Ruslan Ivanov
- city
- Almaty
- NULL
- created_on
- 2026-08-28
7
- name
- Zarina Kaliyeva
- city
- Karaganda
- zarina@example.org
- created_on
- 2026-09-10
8
- name
- Sergey Pak
- city
- Astana
- sergey@example.com
- created_on
- 2026-09-30
Таблицы объединяются в базу данных, а базой управляет СУБД — система управления базами данных. PostgreSQL работает как сервер: приложение (сайт, CRM, мобильный бэкенд) подключается к нему по сети, отправляет запросы и получает ответы. Сам сервер может обслуживать много подключений одновременно.
Первичный ключ: как отличить одну запись от другой
Первичный ключ (primary key) — столбец или набор столбцов, по которому строку можно однозначно найти. По документации PostgreSQL первичный ключ требует двух вещей одновременно: значения уникальны и не бывают NULL. Ключ у таблицы может быть только один, и база автоматически строит для него индекс, чтобы поиск по ключу был быстрым.
Почему бы не взять в качестве ключа имя или телефон? Имена повторяются: в магазине легко окажутся две клиентки по имени Айгерим. Телефон и e-mail меняются, а ещё их может не быть вовсе. Если на изменчивое значение ссылаются заказы, при смене номера придётся переписывать все связанные записи. Поэтому ключ обычно делают отдельным числовым столбцом id, который ничего не значит для людей и никогда не меняется.
CREATE TABLE clients (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
city text,
email text UNIQUE,
created_on date NOT NULL DEFAULT current_date
);
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
client_id bigint NOT NULL REFERENCES clients (id),
ordered_on date NOT NULL,
status text NOT NULL
CHECK (status IN ('new', 'paid', 'shipped', 'cancelled')),
promo_code text
);id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY— ключ; номера выдаёт база, вручную вписать его нельзяname text NOT NULL— имя обязательно, строка без имени не сохранитсяemail text UNIQUE— два клиента с одним e-mail невозможны, но e-mail можно не указыватьcreated_on date NOT NULL DEFAULT current_date— если дату не передали, база подставит сегодняшнюю
Ключ может состоять и из нескольких столбцов. В таблице позиций заказа order_items ключ — пара (order_id, product_id): один и тот же товар не может дважды попасть в один заказ отдельной строкой, вместо этого растёт количество.
Внешний ключ: связь клиента и заказа
В заказе не хранят имя и город клиента — только его номер в столбце client_id. Ограничение REFERENCES clients (id) превращает этот столбец во внешний ключ (foreign key): значение обязано совпадать с первичным ключом какой-то строки в таблице клиентов. Документация называет это ссылочной целостностью — в базе не появятся «осиротевшие» заказы.
- 01clients
- 02orders
- 03order_items
- 04products
Проверим, что база действительно следит за связью. Попробуем создать заказ клиенту с номером 99, которого нет, а затем удалить клиента, у которого есть заказы:
INSERT INTO orders (client_id, ordered_on, status)
VALUES (99, '2026-10-10', 'new');
-- ERROR: insert or update on table "orders" violates foreign key constraint "orders_client_id_fkey"
-- DETAIL: Key (client_id)=(99) is not present in table "clients".DELETE FROM clients WHERE id = 1;
-- ERROR: update or delete on table "clients" violates foreign key constraint "orders_client_id_fkey" on table "orders"
-- DETAIL: Key (id)=(1) is still referenced from table "orders".Что делать с зависимыми строками при удалении, решает проектировщик базы. По умолчанию удаление запрещено, пока на строку кто-то ссылается. Вариант ON DELETE CASCADE удаляет зависимые строки вместе с главной — он подходит, когда зависимая запись не существует сама по себе. В учебной базе так устроены позиции заказа: удалили заказ — удалились и его строки. А вот товары и заказы — независимые сущности, поэтому удалить товар, который кто-то уже купил, база не даст.
Обратите внимание на таблицу order_items. Один заказ содержит много товаров, а один товар встречается во многих заказах — это связь «многие ко многим». Её всегда раскладывают через промежуточную таблицу с двумя внешними ключами; ровно такую схему (products, orders, order_items) документация PostgreSQL разбирает в разделе об ограничениях. Цена в order_items хранится отдельно от каталога: каталог может подорожать, а сумма старого заказа меняться не должна.
1
- product_id
- 1
- quantity
- 1
- price
- 12900.00
1
- product_id
- 6
- quantity
- 2
- price
- 2500.00
Типы данных и ограничения
Тип столбца — первое правило, которое база проверяет при записи: в столбец даты не попадёт строка «завтра», в числовой — «двенадцать». Для учебной базы хватает нескольких типов:
bigint, integer
- Что хранит
- Целые числа (8 и 4 байта)
- Пример в учебной базе
id, количество товара, вес в граммах
numeric(10, 2)
- Что хранит
- Точное десятичное число: до 10 цифр, из них 2 после запятой
- Пример в учебной базе
- Цена
12900.00
text
- Что хранит
- Строка любой длины
- Пример в учебной базе
- Имя, город, статус заказа
date
- Что хранит
- Календарная дата
- Пример в учебной базе
- Дата заказа
2026-09-02
boolean
- Что хранит
- Истина или ложь
- Пример в учебной базе
- Флаг «подписан на рассылку» (в учебной базе нет)
Типа часто недостаточно: цена — число, но отрицательной быть не должна. Для таких правил служат ограничения (constraints). Если запись их нарушает, PostgreSQL отклоняет её целиком и сообщает, какое правило сработало:
NOT NULL
- Правило
- Значение обязательно
- В учебной базе
- Имя клиента, дата и статус заказа
UNIQUE
- Правило
- Значение не повторяется в таблице
- В учебной базе
- E-mail клиента
CHECK (…)
- Правило
- Логическое условие для значения
- В учебной базе
price > 0, статус только из списка
PRIMARY KEY
- Правило
- Уникально и не
NULL; один на таблицу - В учебной базе
idв каждой таблице
REFERENCES
- Правило
- Значение есть в другой таблице
- В учебной базе
orders.client_id→clients.id
UPDATE orders SET status = 'delivered' WHERE id = 1;
-- ERROR: new row for relation "orders" violates check constraint "orders_status_check"
-- DETAIL: Failing row contains (1, 1, 2026-09-02, delivered, null).INSERT INTO clients (name, email)
VALUES ('Aigerim S.', 'aigerim@example.com');
-- ERROR: duplicate key value violates unique constraint "clients_email_key"
-- DETAIL: Key (email)=(aigerim@example.com) already exists.Одна тонкость: ограничение CHECK считается выполненным, если условие истинно или равно NULL. Поэтому weight_g integer CHECK (weight_g > 0) разрешает не указывать вес у кружки, но не даст записать отрицательный. Если значение обязательно, к CHECK добавляют NOT NULL. Документация советует: в большинстве схем большая часть столбцов должна быть NOT NULL.
СУБД против Excel: что база даёт поверх файла
Таблица в Excel или Google Sheets тоже состоит из строк и столбцов, и для старта этого часто хватает. Разница проявляется, когда данных и людей становится больше. Подробно о признаках этого момента — в статье «Когда пора уходить от Excel»; здесь — техническая сторона.
Файл-таблица
- Любая ячейка принимает что угодно: текст в столбце цены, дату в произвольном формате
- Связь «заказ — клиент» держится на том, что имя написано одинаково
- Два человека правят файл одновременно — правки перезаписывают друг друга или расходятся по копиям
- Сбой посередине операции оставляет половину изменений
- Отчёт — ручные фильтры, сводные таблицы и формулы
СУБД (PostgreSQL)
- Типы и ограничения проверяются при каждой записи, ошибочная строка не сохранится
- Внешний ключ гарантирует, что заказ ссылается на существующего клиента
- Сервер обслуживает много подключений одновременно: сайт, CRM и бухгалтерия работают с одними данными
- Транзакция выполняет несколько шагов по принципу «всё или ничего»
- Отчёт — запрос на SQL, который повторяется одной командой
Транзакции стоит выделить отдельно. Документация PostgreSQL описывает их так: несколько шагов объединяются в одну операцию «всё или ничего», промежуточное состояние не видно другим пользователям, а при сбое ни один шаг не применяется. Для магазина это значит, что заказ не окажется оплаченным без списания товара со склада. Транзакциям посвящён отдельный урок траектории.
Реляционные и нереляционные базы — кратко
Всё, что описано выше, относится к реляционным базам: данные лежат в таблицах, связи задаются ключами, вопросы задаются на SQL. Самые распространённые реляционные СУБД в веб-разработке — PostgreSQL и MySQL. Как отмечает глоссарий MDN, большинство баз в веб-разработке устроены именно так, а базы с другой моделью данных называют NoSQL.
Реляционная
- Как хранит
- Таблицы со строгой схемой и связями
- Пример СУБД
- PostgreSQL, MySQL
- Типичная задача
- Клиенты, заказы, оплаты, учёт
Ключ — значение
- Как хранит
- Значение по ключу, часто в памяти
- Пример СУБД
- Redis
- Типичная задача
- Кеш, сессии, счётчики, очереди
Документная
- Как хранит
- Документы JSON-подобной структуры
- Пример СУБД
- MongoDB
- Типичная задача
- Записи с меняющимся набором полей
Это не соревнование: в одном проекте часто работают обе модели — PostgreSQL хранит заказы, Redis кеширует частые ответы. Для учёта, где важны связи и точность, начинают с реляционной базы. Когда и зачем добавлять NoSQL, разбирается в отдельном сравнении направления «Базы данных».
Как SQL «спрашивает» базу
SQL — язык запросов к реляционным базам. Его главная особенность: вы описываете, какой результат нужен, а не как его получить. Не нужно писать «открой таблицу заказов, переберите строки, для каждой найди клиента» — достаточно назвать таблицы, связь и условие. Как именно читать данные, решает сама СУБД.
SELECT o.id, o.ordered_on, c.name, o.status
FROM orders AS o
JOIN clients AS c ON c.id = o.client_id
WHERE c.city = 'Almaty'
ORDER BY o.ordered_on;1
- ordered_on
- 2026-09-02
- name
- Aigerim Seitova
- status
- shipped
3
- ordered_on
- 2026-09-14
- name
- Aigerim Seitova
- status
- paid
4
- ordered_on
- 2026-09-20
- name
- Madina Omarova
- status
- cancelled
7
- ordered_on
- 2026-10-03
- name
- Madina Omarova
- status
- shipped
8
- ordered_on
- 2026-10-05
- name
- Ruslan Ivanov
- status
- new
9
- ordered_on
- 2026-10-08
- name
- Aigerim Seitova
- status
- paid
Запрос читается почти как предложение: выбрать номер, дату, имя и статус из заказов, присоединив клиентов по client_id, где город — Almaty, отсортировав по дате. Каждую часть — SELECT, WHERE, ORDER BY, JOIN — мы разберём по шагам в следующих уроках на этой же учебной базе. Её можно поднять одной командой в Docker и повторить все примеры у себя.
Как проверено4 · пройдено1 · не запускалосьсверено 11 октября 2026 г.
- запущено, работает
Схема clients/orders (фрагмент shop.sql)
shop.sql выполнен в docker postgres:17-alpine (PostgreSQL 17.11) через docker exec -i kb-pg-db0103 psql -U postgres < shop.sql: CREATE TABLE ×4, INSERT 8/9/10/15; фрагмент вставлен в статью скриптом из того же файла
- запущено, работает
Ошибки ограничений: FK (client_id 99), удаление клиента с заказами, CHECK статуса, UNIQUE e-mail
каждый запрос выполнен через psql -X -P null=NULL; текст ошибок вставлен в статью из файлов вывода без правок
- запущено, работает
Таблицы-результаты (clients, order_items заказа 1, запрос с JOIN по Алматы)
таблицы в статье собраны парсером из реального вывода psql (выравнивание по строке с дефисами), без ручного набора
- запущено, работает
ON DELETE CASCADE для order_items
BEGIN; DELETE FROM orders WHERE id = 1; count(order_items) = 0; ROLLBACK — утверждение в тексте подтверждено, код в статью не включён
- не запускалось
Упражнение «Схема салона на бумаге»
упражнение без кода (схема на бумаге), решение — текстовое описание таблиц
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.
