Реляционная база данных: что это, ключи, связи и пример схемы с JOIN

Реляционная база данных: что это, ключи, связи и пример схемы с JOIN Полезное

Реляционная база данных — это база, в которой данные хранятся в виде таблиц (отношений) со строго заданными столбцами, а связи между таблицами выражаются через значения ключей, а не через указатели или вложенность. Управляет такой базой реляционная СУБД (PostgreSQL, MySQL, SQLite, SQL Server, Oracle), запросы к ней пишут на языке SQL.

Ниже — словарь модели, ключи и типы связей, небольшая схема интернет-магазина с запросом через 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 в текстовом поле. Если все данные удобно ложатся в столбцы, отдельные таблицы обычно проще проверять ограничениями.

OTUS Журнал