Реляционная база данных — это база, в которой данные хранятся в виде таблиц (отношений) со строго заданными столбцами, а связи между таблицами выражаются через значения ключей, а не через указатели или вложенность. Управляет такой базой реляционная СУБД (PostgreSQL, MySQL, SQLite, SQL Server, Oracle), запросы к ней пишут на языке SQL.
Содержание
- Откуда название: отношение, а не «связь»
- Ключи: как строки находят друг друга
- Пример: схема магазина и запрос с JOIN
- Целостность: что проверяет СУБД, а что нет
- Нормализация кратко
- Транзакции и ACID
- Популярные реляционные СУБД
- Когда реляционная модель подходит, а когда нет
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — словарь модели, ключи и типы связей, небольшая схема интернет-магазина с запросом через JOIN, нормализация, транзакции ACID и честная граница: где реляционная модель удобна, а где нет. Примеры прогнаны 23.09.2026 в SQLite 3.51 и PostgreSQL 18.6.
Откуда название: отношение, а не «связь»
Частая путаница: «реляционная» кажется производным от «связей между таблицами». На самом деле термин идет от математического понятия relation (отношение), которым Эдгар Кодд в статье 1970 года назвал саму таблицу. Связи между таблицами (relationship) — отдельное понятие, к названию модели отношения не имеющее.
Мини-словарь, чтобы дальше не путаться:
| Термин модели | Как говорят на практике | Что это |
|---|---|---|
| Отношение (relation) | Таблица | Набор однотипных записей с фиксированными столбцами |
| Кортеж (tuple) | Строка, запись | Один объект: конкретный клиент, конкретный заказ |
| Атрибут | Столбец, поле | Свойство объекта: имя, цена, дата |
| Домен | Тип + ограничения | Множество допустимых значений атрибута |
Важная деталь: домен — это не синоним атрибута. Атрибут «цена» принимает значения из домена «целые числа больше нуля». В SQL домен обычно задают типом столбца и ограничениями NOT NULL, CHECK.
Строгая реляционная модель и SQL-таблицы — не одно и то же. Упрощенно считают их равными, но у SQL есть отступления: таблица может содержать одинаковые строки, если нет ключа, а значения NULL вводят трехзначную логику. Это стоит помнить, когда запрос «теряет» строки с пустыми значениями.
Ключи: как строки находят друг друга
- Первичный ключ (PRIMARY KEY) — столбец или набор столбцов, однозначно определяющий строку. По стандарту он уникален и не допускает NULL.
- Альтернативный (потенциальный) ключ — еще один уникальный признак, например email клиента. В SQL его объявляют через
UNIQUE. - Внешний ключ (FOREIGN KEY) — столбец, значение которого должно существовать в первичном или уникальном ключе другой таблицы. Так выражается связь.
- Составной ключ — ключ из нескольких столбцов, например пара «заказ + товар».
Три типа связей строятся на этих ключах:
| Связь | Пример | Как устроена |
|---|---|---|
| Один к одному | Сотрудник — пропуск | Внешний ключ с ограничением UNIQUE |
| Один ко многим | Клиент — заказы | Внешний ключ в таблице «многих» (orders.customer_id) |
| Многие ко многим | Заказы — товары | Промежуточная таблица с двумя внешними ключами |
Пример: схема магазина и запрос с JOIN
Четыре таблицы: клиенты, товары, заказы и позиции заказов. Позиции — промежуточная таблица связи «многие ко многим». Скрипт одинаково выполняется в SQLite и PostgreSQL.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
price_rub INTEGER NOT NULL CHECK (price_rub > 0)
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
created_at TEXT NOT NULL
);
CREATE TABLE order_items (
order_id INTEGER NOT NULL REFERENCES orders(id),
product_id INTEGER NOT NULL REFERENCES products(id),
qty INTEGER NOT NULL CHECK (qty > 0),
PRIMARY KEY (order_id, product_id)
);
INSERT INTO customers VALUES (1, 'Анна', 'anna@example.com'), (2, 'Борис', 'boris@example.com');
INSERT INTO products VALUES (1, 'Клавиатура', 3500), (2, 'Мышь', 1200), (3, 'Монитор', 18900);
INSERT INTO orders VALUES (10, 1, '2026-09-01'), (11, 1, '2026-09-05'), (12, 2, '2026-09-07');
INSERT INTO order_items VALUES (10, 1, 1), (10, 2, 2), (11, 3, 1), (12, 2, 1);
Цена хранится целым числом рублей, чтобы пример был переносимым. Для копеек в PostgreSQL, MySQL и SQL Server берут NUMERIC/DECIMAL, а в SQLite — целое число в копейках.
Теперь соберем сумму каждого заказа из трех связанных таблиц:
SELECT c.name,
o.id AS order_id,
SUM(oi.qty * p.price_rub) AS total_rub
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
GROUP BY c.name, o.id
ORDER BY o.id;
Результат в PostgreSQL 18 (в SQLite те же числа):
name | order_id | total_rub
-------+----------+-----------
Анна | 10 | 5900
Анна | 11 | 18900
Борис | 12 | 1200
(3 rows)
Как это читать: заказ 10 — одна клавиатура за 3500 и две мыши по 1200, итого 5900. JOIN сопоставляет строки по равенству ключей, GROUP BY сворачивает позиции одного заказа в одну строку. Ни имя клиента, ни цена товара не дублируются в заказе — они подтягиваются по ключам в момент запроса.
Целостность: что проверяет СУБД, а что нет
Внешний ключ должен не дать создать заказ несуществующего клиента. Проверим вставку с customer_id = 99:
INSERT INTO orders VALUES (13, 99, '2026-09-10');
PostgreSQL 18 отклоняет строку:
ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"
DETAIL: Key (customer_id)=(99) is not present in table "customers".
А SQLite по умолчанию эту строку принимает — заказ 13 появляется в таблице. Причина: проверка внешних ключей в SQLite выключена, пока на соединении не выполнена команда PRAGMA foreign_keys = ON;. С ней SQLite 3.51 выдает FOREIGN KEY constraint failed. Настройка действует на соединение, поэтому приложение включает ее при каждом подключении.
Отсюда практическое правило: ограничения в схеме защищают данные только тогда, когда конкретная СУБД их действительно проверяет. После создания схемы стоит сделать один заведомо неверный INSERT и убедиться, что он отклонен.
Нормализация кратко
Нормализация — это разбиение данных на таблицы так, чтобы каждый факт хранился в одном месте. Если хранить заказы одной плоской таблицей «имя клиента, email, товар, цена», то смена email потребует правки во всех заказах клиента, а пропущенная строка даст противоречие (аномалия обновления).
- 1НФ: в ячейке одно значение, без списков «клавиатура, мышь» в одном поле.
- 2НФ: при составном ключе каждый неключевой столбец зависит от всего ключа. Цена товара зависит только от
product_id, поэтому она вproducts, а не вorder_items. - 3НФ: неключевые столбцы не зависят друг от друга. Email зависит от клиента, а не от заказа, поэтому он в
customers.
Граница упрощения: на практике цену часто сознательно копируют в позицию заказа в момент покупки, потому что каталожная цена потом меняется, а заказ должен помнить старую. Это не ошибка нормализации, а другой факт — «цена на момент заказа». Для отчетов иногда денормализуют ради скорости, и это осознанный компромисс.
Транзакции и ACID
Транзакция — группа операций, которая применяется целиком или не применяется совсем. Свойства ACID:
- Atomicity (атомарность) — все или ничего;
- Consistency (согласованность) — после транзакции соблюдены все ограничения схемы;
- Isolation (изоляция) — параллельные транзакции влияют друг на друга только в пределах выбранного уровня изоляции;
- Durability (долговечность) — подтвержденные изменения переживают сбой.
Попробуем создать заказ 14, где вторая позиция нарушает CHECK (qty > 0):
BEGIN;
INSERT INTO orders VALUES (14, 2, '2026-09-12');
INSERT INTO order_items VALUES (14, 1, 1);
INSERT INTO order_items VALUES (14, 3, 0);
COMMIT;
SELECT COUNT(*) AS orders_14 FROM orders WHERE id = 14;
PostgreSQL после ошибки переводит транзакцию в прерванное состояние, и COMMIT фактически выполняет откат:
ERROR: new row for relation "order_items" violates check constraint "order_items_qty_check"
DETAIL: Failing row contains (14, 3, 0).
ROLLBACK
orders_14
-----------
0
(1 row)
В SQLite тот же скрипт, выполненный в консоли sqlite3 без остановки на ошибке, ведет себя иначе: отменяется только упавший INSERT, а COMMIT сохраняет заказ 14 и первую позицию. Получается заказ без одной позиции. Исправление — при любой ошибке внутри транзакции приложение явно выполняет ROLLBACK вместо COMMIT. С ROLLBACK в SQLite запрос выше возвращает 0.
Вывод из сравнения: атомарность гарантирует откат, когда транзакция откатывается, а решение «откатить или подтвердить» после ошибки в разных СУБД принимается по-разному. Надежный код не полагается на поведение по умолчанию.
Популярные реляционные СУБД
| СУБД | Модель развертывания | Где встречается |
|---|---|---|
| PostgreSQL | Клиент-сервер, открытая лицензия PostgreSQL License | Веб-сервисы, аналитика, геоданные; объектно-реляционная, есть JSONB |
| MySQL | Клиент-сервер, GPL и коммерческая лицензия Oracle | Веб-приложения, CMS |
| SQLite | Встраиваемая библиотека, без сервера | Мобильные и десктопные приложения, тесты, локальные файлы данных |
| Microsoft SQL Server | Клиент-сервер, коммерческая, есть бесплатная редакция Express | Корпоративные системы на стеке Microsoft |
| Oracle Database | Клиент-сервер, коммерческая | Крупные корпоративные и банковские системы |
MySQL создала шведская компания MySQL AB, а к Oracle продукт перешел вместе с покупкой Sun Microsystems. Язык SQL у всех общий по основе, но диалекты различаются: автоинкремент, типы дат, логический тип и поведение ограничений придется сверять при переносе схемы.
Когда реляционная модель подходит, а когда нет
Реляционная база хорошо подходит, если данные однородны по структуре, между сущностями много связей, важны ограничения целостности и транзакции: заказы, платежи, учет, пользователи и права. Хранить картинки или JSON она тоже может (типы BLOB, JSONB), так что «только структурированные данные» — устаревшее упрощение.
Другие модели выбирают, когда структура записей сильно различается, нужен очень высокий поток записей с чтением по ключу, данные по сути граф или требуется горизонтальное масштабирование на много серверов. Масштабировать реляционную базу горизонтально тоже возможно (репликация, шардирование), но это заметно сложнее в эксплуатации.
Выводы
- Реляционная база хранит данные в таблицах-отношениях, а связи задает значениями ключей: первичный идентифицирует строку, внешний ссылается на строку другой таблицы.
- «Многие ко многим» всегда реализуется промежуточной таблицей с двумя внешними ключами, как
order_itemsв примере. - Нормализация убирает дублирование фактов, но осознанная копия (цена на момент заказа) — не ошибка.
- Ограничения и транзакции работают так, как их реализует конкретная СУБД: в SQLite внешние ключи по умолчанию выключены, а ошибка внутри транзакции не откатывает ее автоматически.
Где применяется / связь с практикой
С реляционными базами работают бэкенд-разработчики, аналитики данных, тестировщики и администраторы: проектируют схемы, пишут запросы с JOIN и группировкой, разбирают медленные запросы и нарушения целостности. Хорошая тренировка — повторить схему из статьи, добавить таблицу категорий товаров и написать запрос «выручка по категориям за месяц».
Освойте тему на практике
Системно освоить SQL — от SELECT и JOIN до индексов, оконных функций и транзакций — можно на курсе «SQL для разработчиков и аналитиков». Разобрать отдельные темы с преподавателями вживую можно на бесплатных открытых уроках Otus.
FAQ
Чем реляционная база отличается от СУБД?
База — это сами данные и их схема, СУБД — программа, которая хранит базу, выполняет запросы и проверяет ограничения. Одна СУБД PostgreSQL может обслуживать много баз.
Обязательно ли у таблицы должен быть первичный ключ?
SQL позволяет создать таблицу без ключа, но тогда в ней возможны полностью одинаковые строки и нельзя надежно адресовать одну запись. На практике первичный ключ задают почти всегда.
Можно ли хранить JSON в реляционной базе?
Да: в PostgreSQL есть типы json и jsonb с индексами, в MySQL — тип JSON, в SQLite — функции для JSON в текстовом поле. Если все данные удобно ложатся в столбцы, отдельные таблицы обычно проще проверять ограничениями.



