- Общие понятия баз данных
- Реляционная модель данных
- Масштабирование баз данных
- Паттерны баз данных
- Движки баз данных
- Проблема n + 1
- Оптимизация SQL-запросов
Базой данных (англ. database), БД (англ. DB) называют организованную структуру, которая предназначена для хранения, чтения и изменения данных.
Самой простой базой данных может послужить обычный текстовый файл, однако искать и изменять данные в такой "базе" было бы как минимум неудобно.
Транзакция (англ. transaction) — это логическая единица (logical unit), которая осуществляет доступ к базе данных и, возможно, изменяет данные в ней.
Транзакции работают с данными, используя операции чтения и записи.
- Атомарность (англ. Atomicity) гарантирует, что транзакция не может быть зафиксирована частично: либо выполняются все подоперации транзакции, либо ни одна. Если какая-то операция из последовательности не проходит, то происходит откат (англ.
rollback) всей последовательности. - Согласованность, консистентность (англ. Consistency). Каждая успешная транзакция фиксирует только допустимый для системы результат. База данных должна быть согласована до и после транзакции, во время проведения транзакции согласованность не требуется.
- Изолированность (англ. Isolation). Способность базы данных гарантировать, что параллельно выполняющиеся транзакции не мешают друг другу. Во время выполнения транзакции другие параллельные транзакции не должны оказывать влияния на её результат. Требование "дорогое", поэтому в реальных базах транзакции изолируются не полностью: создаются уровни изолированности.
- Долговечность (англ. Durability). Если пользователь получил подтверждение от системы, что транзакция выполнена, он может быть уверен, что сделанные им изменения не будут отменены из-за какого-либо сбоя. Изменения, сделанные успешно завершённой транзакцией, должны остаться сохранёнными после возвращения системы в работу.
Пусть деньги переводятся с одного счёта на другой. Используются две основные операции: вывод денег с одного счёта 1000$ - 100$ = 400$, зачисление их на другой счёт 500$ + 100$ = 600$. Если первая операция удалась, а вторая нет, то происходит откат, поскольку в противном случае имеем несогласованность (inconsistency) данных до и после транзакции: до транзакции на счетах в сумме было 1500$, после — 1400$.
BEGIN TRANSACTION;
-- Операция 1
INSERT INTO Employees (EmployeeID, FirstName, LastName, Position, Salary)
VALUES (104, 'Петр', 'Петров', 'Разработчик', 80000);
-- Установка точки сохранения
SAVEPOINT AfterInsert;
-- Операция 2
UPDATE Employees
SET Salary = Salary * 1.05
WHERE EmployeeID = 104;
-- Возникла ошибка, откат к точке сохранения
ROLLBACK TO SAVEPOINT AfterInsert;
-- Фиксация транзакции
COMMIT;Уровни изолированности транзакций (англ. Transaction Isolation Levels) — фундаментальная концепция в реляционных базах данных, которая определяет, как транзакции изолированы друг от друга и какие “аномалии” могут или не могут возникнуть при одновременном доступе к данным.
Уровни:
- Чтение неподтверждённых данных (англ.
Read Uncommitted) - можно читать неподтверждённые данные (грязное чтение). Грязные чтения, неповторяющиеся чтения, фантомные чтения возможны. - Чтение подтверждённых данных (англ.
Read Committed) - можно читать лишь подтверждённые данные (грязное чтение невозможно). Неповторяющиеся чтения, фантомные чтения возможны. - Повторяемое чтение (англ.
Repeatable Read) - все данные, прочитанные в транзакции, будут одинаковыми при повторном чтении. Грязные и неповторяющиеся чтения невозможны. Возможны фантомные чтения. - Сериализуемость (англ.
Serializable) - полная изолированность, максимальная защита от всех аномалий.
Примеры применения:
-- PostgreSQL
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- MySQL
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;- Грязное чтение (англ.
Dirty read) - транзакции читают данные, которые ещё не подтверждены другой транзакцией. - Неповторяющееся чтение (англ.
Non-repeatable read) - повторное чтение тех же данных возвращает различные значения. - Фантомное чтение (англ.
Phantom read) - повторное чтение тех же данных возвращает новые строки, добавленные другой транзакцией.
- Read Committed - максимальная производительность, мало конфликтов.
- Repeatable read - стабильное чтение, терпимы фантомы
- Serializable - критичные данные, важно избегать любых конфликтов
- Об индексе базы данных
- Проблема дублирования данных
- Основные типы индексов
Индекс базы данных (англ. database index) — это структура данных, ускоряющая операции поиска (англ. speed up querying) в заданной таблице (или коллекции) базы данных за счёт хранения дополнительной информации в базе (говоря простыми словами, тех же данных таблицы, но в другом виде, в упорядоченном).
Индекс — это дополнительная структура данных, например, в виде дерева, которая позволяет быстро найти нужные строки, не перебирая всю таблицу (англ. Full Table Scan). Индексы хранят упорядоченные или быстро доступные ссылки на строки таблицы. Это похоже на оглавление в книге, которое позволяет быстро найти нужную страницу.
По умолчанию записи в таблице (коллекции) могут храниться произвольно (неупорядоченно). И если таблица довольно большая, то последовательный просмотр всех записей может занимать довольно продолжительное время. Индексы приводят данные в такой вид (упорядочивают их), чтобы это ускорило поиск. Например, структура индекса может быть представлена сбалансированным деревом поиска, и тогда скорость поиска будет логарифмичной O(log n) вместо линейной O(n).
Индекс формируется из значений одного или нескольких столбцов таблицы (полей документов коллекции) и указателя на соответствующие строки таблицы (документы коллекции). Также иногда индексы могут создаваться из выражений.
Наиболее распространённый тип индекса (чаще всего используется по умолчанию). Организован в виде сбалансированного дерева поиска. Когда использовать:
- Поиск точных значений (например,
WHERE id = 10) - Поиск диапазонов (
WHERE date BETWEEN x AND y) - Сортировка (
ORDER BY) JOINпо индексированным полям
Недостатки:
- Менее эффективен при поиске текста по шаблону (например, с
LIKE '%search%') - Ненужное создание множества таких индексов может снизить производительность при вставке и обновлении записей
Используют хеш-функции для быстрого поиска по точному соответствию ключа.
Когда использовать:
- Очень быстрый поиск по точному совпадению (например,
WHERE username = 'user123')
Недостатки:
- Не поддерживают поиск по диапазону (
BETWEEN,<,>) или сортировку - Не подходят для поиска частичных совпадений
- Чаще используются в специализированных БД (например, in-memory хранилищах)
Используются для поиска слов или фраз в тексте. Когда использовать:
- Полнотекстовый поиск по большим текстовым полям (
LIKEнедостаточно эффективен)
Примеры СУБД:
- MySQL (MATCH AGAINST)
- PostgreSQL (GIN, GiST индексы)
Состоят из нескольких полей таблицы.
Когда использовать:
- Запросы, включающие несколько полей одновременно (WHERE name = X AND date = Y)
- Частые сложные условия с множеством критериев
Важно:
- Порядок столбцов в индексе очень важен. Первым указывается поле, по которому чаще всего происходит поиск или фильтрация.
Не допускает повторяющихся значений.
Когда использовать:
- Поля, значения которых должны быть уникальными (например,
email,phone,username)
Индексы, применяемые только к части строк таблицы (по условию). Когда использовать:
- Если часто выбирается лишь небольшая часть данных (например, только активные пользователи)
Используются для специфических типов данных (пространственные данные, JSON и пр.). Когда использовать:
- Геоданные (например, геолокации, поиск по карте)
- JSON-данные с поиском по внутренним полям
Количество индексов прямо влияет на производительность
- Много индексов:
- ➕ Ускоряет многие запросы на чтение
- ➖ Значительно замедляет операции записи (
INSERT, UPDATE, DELETE), увеличивает размер базы данных, повышает нагрузку на сервер.
- Недостаточно индексов:
- ➕ Быстрая запись данных
- ➖ Медленные запросы на чтение, полное сканирование таблиц, неоптимальная нагрузка на сервер.
Если у таблицы 5 индексов, то при добавлении новой строки база данных должна обновить все 5 индексов, что занимает больше времени, чем без индексов или с одним индексом.
Создавайте индексы только на те поля, которые часто используются в запросах. Избегайте индексов на полях с низкой селективностью (например, пол (мужчина/женщина) — плохой кандидат для индекса, так как пользы почти не принесёт).
Правильное использование индексов — это баланс между скоростью чтения, скоростью вставки и обновления данных, а также объёмом используемого места.
Индексы ускоряют чтение, но замедляют операции записи (INSERT, UPDATE, DELETE).
- Используйте индексы осознанно: больше индексов ≠ всегда лучше.
- Регулярно проверяйте производительность запросов (с помощью EXPLAIN) и оптимизируйте набор индексов.
Курсор (англ. cursor) — управляющая структура, которая производит обход записей (англ. traversal) в базе данных.
Принцип работы с курсором похож на работу с итератором (англ. iterator), поскольку курсор позволяет последовательно обработать каждую строку (каждый документ) из выбранного набора строк (документов).
Курсор также можно рассматривать как указатель (pointer) на одну строку таблицы (на один документ). Курсор может ссылаться только на одну строку (один документ) в один момент времени, но может перемещаться к другим строкам при необходимости.
Курсоры подразделяются на явные и неявные.
Явный курсор создаётся разработчиком вручную, неявный курсор создаётся системой автоматически.
const usersCursor = db.users.find({ role: 'admin' });
/* последовательный перебор всех документов результирующего набора в цикле while */
while (usersCursor.hasNext()) {
const user = usersCursor.next();
console.log(user);
}# объявление временных переменных, которые будут использованы для записи данных курсора
DECLARE @id VARCHAR(50);
DECLARE @role VARCHAR(50);
# объявление явного курсора, который будет брать данные столбцов id, role из таблицы users
DECLARE @users_cursor CURSOR FOR
SELECT id, role
FROM users
# открытие курсор (наполнение его данными)
OPEN @users_cursor
# последовательное извлечение строк из результирующего набора в цикле WHILTE,
# запись значений в переменные и вывод этих переменных
WHILE @@FETCH_STATUS = 0
BEGIN
FETCH NEXT FROM @users_cursor INTO @id, @role
PRINT @id + ' ' + @role
END
# после извлечения всех строк следует закрыть курсор и освободить занимаемые им ресурсы
CLOSE @users_cursor
# иногда нужно вручную освобождать память курсора (зависит от реализации SQL)
DEALLOCATE @users_cursorВозможные варианты перемещений с FETCH по результирующему набору строк.
FIRST— первая строка.NEXT— следующая строка после текущей.PRIOR— строка, находящаяся перед текущей.LAST— последняя строка.ABSOLUTE int— строка по её абсолютному порядковому номеруintпри отсчёте с начала (конца), если передintзнак + (-). Если указать 0, то ничего не вернётся.RELATIVE int— перемещение наintстрок вперёд (назад) от текущей, если передintзнак + (-). Если указать 0, то вернётся текущая строка.
Изменение данных таблицы при помощи курсора и CURRENT. Курсор должен быть открыт для операции.
UPDATE new_users
SET ...
WHERE CURRENT OF @sers_cursorАналогично можно делать с DELETE.
Когда одного сервера базы данных перестаёт хватать, его можно усилить (вертикальное масштабирование) или разделить работу между серверами (горизонтальное) — об этом в заметке про масштабирование. Горизонтально базы масштабируют тремя приёмами:
- репликация копирует одни и те же данные на несколько серверов;
- шардинг раскладывает разные данные по разным серверам;
- партиционирование делит большую таблицу на части.
Приёмы сочетаются: например, в MongoDB каждый шард должен быть развёрнут как набор реплик (документация).
Репликация (англ. replication) — постоянное копирование данных с одного сервера на другие. Сервер, который хранит копию, называют репликой (англ. replica).
Обычно все изменения принимает один сервер — ведущий (англ. primary). Остальные — ведомые (англ. standby, secondary) — повторяют его изменения и отвечают на чтение. В MongoDB, например, ведущий получает все операции записи, а вторичные узлы повторяют журнал его операций (документация).
В старых текстах ведущего и ведомых называют мастером (англ. master) и слейвами (англ. slaves). Документация PostgreSQL и MongoDB использует слова primary, standby и secondary.
Главный вопрос — когда ведущий считает запись завершённой.
Асинхронная репликация (англ. asynchronous replication): ведущий подтверждает запись сразу, а ведомые получают изменения чуть позже. В PostgreSQL потоковая репликация по умолчанию асинхронная: между фиксацией транзакции на ведущем и появлением изменений на ведомом есть небольшая задержка (документация). Такую задержку называют отставанием реплики (англ. replication lag). MongoDB предупреждает: чтение со вторичных узлов может вернуть данные, которые ещё не отражают состояние ведущего.
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
class Replica {
readonly data = new Map<string, string>();
}
/* ведущий подтверждает запись сразу, а ведомым отправляет её с задержкой */
class Primary extends Replica {
constructor(private readonly standbys: Replica[], private readonly lagMs: number) {
super();
}
write(key: string, value: string): void {
this.data.set(key, value);
for (const standby of this.standbys) {
setTimeout(() => standby.data.set(key, value), this.lagMs);
}
}
}
const standby = new Replica();
const primary = new Primary([standby], 100);
primary.write('user:1:name', 'Max');
console.log(primary.data.get('user:1:name')); // Max
console.log(standby.data.get('user:1:name')); // undefined
await sleep(150);
console.log(standby.data.get('user:1:name')); // MaxПользователь сменил имя, обновил страницу и видит старое: чтение ушло на ведомого, который ещё не догнал ведущего. Выходы: читать только что изменённые данные с ведущего, показывать пользователю то, что вернул ответ на запись, или включить синхронную репликацию.
Синхронная репликация (англ. synchronous replication): фиксация ждёт, пока запись не окажется в журнале на диске и ведущего, и ведомого. Потерять данные можно, только если оба сервера упадут одновременно. Цена — время ответа: каждая пишущая транзакция ждёт как минимум время пути туда и обратно между серверами, а транзакции только для чтения не ждут (PostgreSQL).
Если выходит из строя ведомый, чтение берут на себя остальные серверы. Восстановившись, он догоняет ведущего и снова отвечает на запросы.
Если выходит из строя ведущий, одного из ведомых повышают до ведущего — это переключение (англ. failover). В MongoDB оно происходит само: если ведущий недоступен, подходящий вторичный узел проводит выборы (англ. election) нового ведущего (документация). PostgreSQL отказ ведущего сам не обнаруживает — для этого нужны внешние инструменты, а повысить ведомого можно командой pg_ctl promote или функцией pg_promote() (документация).
Опасен старый ведущий: если он вернётся и не узнает, что его заменили, оба сервера будут считать себя ведущими, а это ведёт к путанице и в итоге к потере данных. Механизм, который сообщает старому ведущему, что он больше не ведущий, иногда называют STONITH (англ. Shoot The Other Node In The Head). Чтобы вернуться к нормальной работе, нового ведомого создают — на бывшем ведущем, когда тот поднимется, или на другой машине: ведущий и ведомый меняются ролями (PostgreSQL).
- Масштабирование чтения: запросы на чтение можно отправлять на ведомых. В PostgreSQL ведомый, который отвечает на запросы только для чтения, называют горячим резервом (англ.
hot standby). Запись так не масштабируется: все изменения по-прежнему идут через ведущего. - Отказоустойчивость: копия данных заменит упавший сервер.
- Отдельные задачи: тяжёлые отчёты можно запускать на отдельной реплике, чтобы они не мешали основной работе. PostgreSQL прямо описывает такие серверы для отчётов: они только снимают с ведущего запросы на чтение и повышать их до ведущего не нужно (документация).
Шардинг (англ. sharding) — распределение данных по нескольким машинам: каждая часть — шард (англ. shard) — хранит своё подмножество данных (MongoDB). Репликация копирует одни и те же данные, а шардинг делит разные, поэтому он распределяет не только чтение, но и запись.
Какой шард хранит запись, решает ключ шардинга (англ. shard key) — одно или несколько полей записи. Например, в социальной сети ключом может быть идентификатор пользователя: все данные одного пользователя окажутся на одном сервере, и их не придётся собирать по частям.
При выборе ключа MongoDB советует смотреть на три вещи (документация):
- кардинальность — сколько разных значений у ключа;
- частоту — как часто встречается каждое значение;
- монотонность — растёт ли ключ всё время. Если ключ только растёт (время создания, счётчик), все новые записи попадают в один и тот же шард, и он становится узким местом для записи.
Два способа разложить данные по ключу:
- по диапазонам (англ.
ranged sharding): близкие значения ключа лежат рядом, поэтому запросы по диапазону («заказы за сентябрь») идут в один шард (MongoDB); - по хешу (англ.
hashed sharding): данные распределяются равномернее, но близкие значения разлетаются по разным шардам, и запрос по диапазону приходится рассылать на все (англ.broadcast) (MongoDB).
Сложнее всего — добавить шард. Если номер шарда — остаток от деления хеша ключа на число шардов, то при переходе с четырёх шардов на пять переедет почти всё:
import { createHash } from 'node:crypto';
const hash = (key: string) => createHash('md5').update(key).digest().readUInt32BE(0);
const keys = Array.from({ length: 10_000 }, (_, i) => `user:${i}`);
/* шард = хеш по модулю числа шардов */
const moved = keys.filter((key) => hash(key) % 4 !== hash(key) % 5).length;
console.log(moved); // 7975Поэтому данные часто делят на фиксированное число слотов (англ. slots), а серверам раздают слоты. Новому серверу отдают часть слотов, и переезжают только их ключи:
const SLOTS = 1024;
const slotOf = (key: string) => hash(key) % SLOTS;
/* было четыре сервера: слот → сервер */
const before = Array.from({ length: SLOTS }, (_, slot) => slot % 4);
/* пятому серверу отдаём каждый пятый слот */
const after = before.map((server, slot) => (slot % 5 === 4 ? 4 : server));
console.log(keys.filter((key) => before[slotOf(key)] !== after[slotOf(key)]).length); // 1942Так устроен Redis Cluster: ключи делятся на 16384 слота по формуле CRC16(key) mod 16384, и каждый ведущий узел отвечает за свою часть слотов (спецификация). MongoDB с версии 5.0 умеет менять ключ шардинга и перераспределять данные командой reshardCollection.
Шарды тоже нужно реплицировать: если у шарда нет копий, отказ одного сервера сделает недоступной его часть данных. Поэтому в MongoDB каждый шард — набор реплик.
Партиционирование (англ. partitioning) — разбиение одной логической таблицы на несколько физических частей — партиций (англ. partitions) (PostgreSQL). Для запросов таблица остаётся одной, а база сама решает, в какую партицию идти. Шардинг — по сути тоже разбиение, но его части лежат на разных серверах; партиции обычно живут в одной базе.
Виды партиционирования в PostgreSQL:
- по диапазону (англ.
range), например по датам: нижняя граница входит в партицию, а верхняя — нет; - по списку (англ.
list): для каждой партиции перечислены значения ключа; - по хешу (англ.
hash): строка попадает в партицию по остатку от деления хеша ключа.
Что даёт партиционирование (PostgreSQL):
- запросы ускоряются, когда нужные строки лежат в одной или нескольких партициях: планировщик пропускает остальные. Это отсечение партиций (англ.
partition pruning); - часто используемые части индексов легче помещаются в память;
- старые данные удаляют целой партицией:
DROP TABLEилиALTER TABLE ... DETACH PARTITIONнамного быстрее массовогоDELETEи не создают работы дляVACUUM; - редко нужные данные можно перенести на дешёвые и медленные носители.
Выгода заметна только на очень больших таблицах. По эмпирическому правилу таблица должна быть больше оперативной памяти сервера базы данных.
Жизненный пример: новостной сайт партиционирует новости по дате публикации. Свежие новости читают чаще архивных, а архив за старый год удаляется одной командой. Вывод ниже — из PostgreSQL 17.
CREATE TABLE news (
id bigint,
published_at date NOT NULL,
title text
) PARTITION BY RANGE (published_at);
CREATE TABLE news_2025 PARTITION OF news FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
CREATE TABLE news_2026 PARTITION OF news FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
INSERT INTO news VALUES (1, '2025-12-31', 'Итоги года'), (2, '2026-09-28', 'Свежая новость');
SELECT tableoid::regclass AS partition, title FROM news;
-- partition | title
-- -----------+----------------
-- news_2025 | Итоги года
-- news_2026 | Свежая новость
EXPLAIN (COSTS OFF) SELECT title FROM news WHERE published_at >= '2026-09-01';
-- Seq Scan on news_2026 news
-- Filter: (published_at >= '2026-09-01'::date)
-- партицию news_2025 планировщик даже не открывает
DROP TABLE news_2025; -- архив за 2025 год удалён целикомТри приёма легко спутать: каждый что-то копирует или делит. Разница — в том, что именно они масштабируют и чем за это платят.
| Критерий | Репликация | Шардинг | Партиционирование |
|---|---|---|---|
| Что делает | копирует все данные на несколько серверов | делит данные между серверами по ключу | делит таблицу на части внутри одной базы |
| Что лежит на каждом сервере | полная копия | своя часть данных | сервер один, а части — в разных партициях |
| Что масштабирует | чтение | чтение, запись и объём данных | мощности не добавляет: ускоряет запросы к части данных и обслуживание |
| Отказ сервера | ведомый заменит ведущего | переживёт, только если каждый шард реплицирован | не защищает |
| Главная сложность | отставание реплик | выбор ключа, запросы во все шарды, перенос данных | запросы без ключа партиционирования читают все партиции |
| Что меняется в приложении | чтение и запись идут на разные серверы | нужно знать, где лежат данные, или нужен маршрутизатор запросов | почти ничего: таблица остаётся одной |
Приёмы не исключают друг друга, и их часто сочетают (пример ниже).
| Задача | Приём | Пример |
|---|---|---|
| чтений намного больше, чем записей | репликация | новостной сайт: статьи читают постоянно, а пишут несколько редакторов |
| сервис должен работать, даже если сервер базы упал | репликация | интернет-магазин: ведомого повышают до ведущего |
| тяжёлые отчёты мешают основной работе | реплика для отчётов | бухгалтерия строит годовой отчёт на отдельной реплике |
| огромная таблица с данными по времени, старое удаляют | партиционирование по диапазону | логи или новости по месяцам: старую партицию удаляют целиком |
| данные делятся по понятному признаку | партиционирование по списку | заказы по странам |
| огромная таблица без естественного признака | партиционирование по хешу | сессии по идентификатору |
| запись или объём данных не помещаются на один сервер | шардинг | мессенджер: сообщения шардируют по чату |
| всё сразу | сочетание | шарды по клиентам, у каждого — реплики, а внутри — партиции по месяцам |
Репликация помогает, когда упирается чтение или нужна отказоустойчивость. Данные при этом помещаются на один сервер: каждая реплика хранит их целиком.
Пример: новостной сайт. Статьи читают постоянно, а пишут редакторы изредка. Запись идёт на ведущего, а чтение — по кругу на реплики:
type Query = { sql: string; readOnly: boolean };
const primaryServer = 'db-primary';
const replicaServers = ['db-replica-1', 'db-replica-2'];
let next = 0;
/* запись — только на ведущего, чтение — по кругу на реплики */
const route = ({ readOnly }: Query) => (readOnly ? replicaServers[next++ % replicaServers.length] : primaryServer);
console.log(route({ sql: "UPDATE articles SET title = 'Новый заголовок' WHERE id = 1", readOnly: false })); // db-primary
console.log(route({ sql: 'SELECT * FROM articles WHERE id = 1', readOnly: true })); // db-replica-1
console.log(route({ sql: 'SELECT * FROM articles ORDER BY id DESC LIMIT 10', readOnly: true })); // db-replica-2Редактор, который только что сохранил статью, может не увидеть правку на реплике — это отставание реплики. Поэтому его запросы сразу после сохранения отправляют на ведущего. В PostgreSQL есть и другой путь: с настройкой synchronous_commit = remote_apply фиксация ждёт, пока синхронные ведомые не применят транзакцию, и она станет видна запросам на них (документация).
Пример: реплика для отчётов. Бухгалтерия строит отчёт за год: запрос читает все заказы и долго нагружает сервер. На отдельной реплике он не замедлит покупателей, а повышать такую реплику до ведущего не нужно (PostgreSQL).
Пример: отказ ведущего. Ночью упал ведущий интернет-магазина: ведомого повышают до ведущего, и магазин снова принимает заказы. Но при асинхронной репликации часть уже подтверждённых транзакций может не успеть дойти до ведомого и потеряется: чем больше отставание в момент отказа, тем больше потери. Важные данные можно писать синхронно: PostgreSQL позволяет задать синхронность для отдельных пользователей, подключений и даже транзакций. Пример из документации: данные клиентов — синхронно, а сообщения в чате — асинхронно, чтобы не замедлять основную нагрузку (документация).
Шардинг нужен, когда один сервер не справляется с записью или не вмещает данные. Цена — ключ шардинга: он определяет, как будут работать запросы, а сменить его потом — значит перенести данные между шардами.
Пример: мессенджер. Главный запрос — открыть чат и показать последние сообщения. Ключ — идентификатор чата: все сообщения одного чата лежат на одном шарде.
const SHARDS = 4;
/* упрощённо: номер шарда — остаток от деления; на практике — слоты, как выше */
const shardOf = (chatId: number) => chatId % SHARDS;
/* все сообщения одного чата — на одном шарде */
console.log([101, 101, 101].map(shardOf)); // [ 1, 1, 1 ]
/* разные чаты расходятся по шардам */
console.log([101, 102, 103, 104].map(shardOf)); // [ 1, 2, 3, 0 ]Открыть чат — запрос в один шард. Найти слово во всех чатах пользователя — запрос во все шарды. MongoDB называет такие запросы scatter-gather: они менее эффективны и не масштабируются линейно, когда шардов становится больше. Исключение — агрегации по большому объёму данных: там параллельная работа всех шардов даже полезна (документация). Поэтому ключ выбирают под самые частые запросы.
Пример: сервис для многих компаний. В SaaS каждая компания — отдельный клиент (англ. tenant), и её данные почти никогда не смешиваются с чужими. Ключ — идентификатор компании: запросы внутри компании идут в один шард. Но одна огромная компания может перегрузить свой шард — это проблема частоты. MongoDB советует в таком случае составной ключ с уникальным или редким значением, например (company_id, user_id).
Пример: плохие ключи. Ошибки, о которых предупреждает MongoDB:
| Ключ | Что не так | Что будет |
|---|---|---|
continent |
мало разных значений | значений всего 7, поэтому больше семи шардов пользы не дадут (пример из документации) |
country в сервисе, где почти все из одной страны |
одно значение встречается слишком часто | шард с этой страной станет узким местом |
created_at |
значение всё время растёт | все новые записи идут в один шард; совет MongoDB — шардинг по хешу |
chat_id, user_id |
всё в порядке | много разных значений, а запросы обычно касаются одного чата или пользователя |
Партиционирование помогает, когда одна таблица стала огромной, а запросы и обслуживание касаются её части: последнего месяца, одной страны. Мощности оно не добавляет — сервер тот же. Пример по диапазону — новости по годам выше, а ниже — два других вида. Вывод — из PostgreSQL 17.
Пример: заказы по странам (по списку). Заказы разных стран лежат в разных партициях, а для остальных есть партиция по умолчанию (DEFAULT). Без неё заказ из страны вне списков не вставится: PostgreSQL ответит ошибкой no partition of relation "orders" found for row.
CREATE TABLE orders (
id bigint,
country text NOT NULL,
total numeric
) PARTITION BY LIST (country);
CREATE TABLE orders_eu PARTITION OF orders FOR VALUES IN ('DE', 'FR', 'PL');
CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('US');
CREATE TABLE orders_other PARTITION OF orders DEFAULT;
INSERT INTO orders VALUES (1, 'DE', 250), (2, 'US', 100), (3, 'AM', 70);
SELECT tableoid::regclass AS partition, country FROM orders ORDER BY id;
-- partition | country
-- --------------+---------
-- orders_eu | DE
-- orders_us | US
-- orders_other | AM
EXPLAIN (COSTS OFF) SELECT sum(total) FROM orders WHERE country = 'DE';
-- Aggregate
-- -> Seq Scan on orders_eu orders
-- Filter: (country = 'DE'::text)Отчёт по одной стране читает одну партицию, а отчёт по всем — все.
Пример: сессии (по хешу). У сессий нет удобного признака для разбиения, а таблица огромная. Хеш раскладывает строки почти поровну, а поиск по id идёт в одну партицию:
CREATE TABLE sessions (id uuid, data jsonb) PARTITION BY HASH (id);
CREATE TABLE sessions_0 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_1 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sessions_2 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sessions_3 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 3);
/* 10 000 сессий с повторяемыми идентификаторами */
INSERT INTO sessions SELECT md5(i::text)::uuid, '{}' FROM generate_series(1, 10000) AS i;
SELECT tableoid::regclass AS partition, count(*) FROM sessions GROUP BY 1 ORDER BY 1;
-- partition | count
-- ------------+-------
-- sessions_0 | 2480
-- sessions_1 | 2526
-- sessions_2 | 2497
-- sessions_3 | 2497
EXPLAIN (COSTS OFF) SELECT data FROM sessions WHERE id = 'a1d0c6e8-3f02-7327-d846-1063f4ac58a6';
-- Seq Scan on sessions_0 sessions
-- Filter: (id = 'a1d0c6e8-3f02-7327-d846-1063f4ac58a6'::uuid)Приёмы решают разные задачи, поэтому большие системы используют их вместе. Например, так может расти интернет-магазин:
- Один сервер. Сначала — индексы, оптимизация запросов и сервер помощнее (вертикальное масштабирование).
- Чтения стало много → реплики для чтения, а заодно отказоустойчивость.
- Таблица
ordersразрослась → партиции по месяцам; старые удаляют или переносят на дешёвые носители. - Записи стало больше, чем выдерживает один ведущий → шардинг по
customer_id. У каждого шарда — свои реплики, а внутри — те же партиции.
orders — шардинг по customer_id
├── шард 1: ведущий + 2 ведомых
│ ├── orders_2026_08 ← партиции по месяцам
│ └── orders_2026_09
└── шард 2: ведущий + 2 ведомых
├── orders_2026_08
└── orders_2026_09
Шардинг делит клиентов между серверами, репликация держит копии каждого шарда для чтения и на случай отказа, а партиции ускоряют запросы за месяц и упрощают удаление старых заказов.
- О реляционной модели данных
- Коротко о главном из теории множеств
- Тип данных (домен)
- Отношение и его структура
- Свойства отношений
- Графическое представление отношения (таблица)
- Ключи
- Связи между отношениями
- Операции над отношениями (реляционные операции)
- Разновидности операций соединения
- Свойства операций над отношениями
- Функциональная зависимость
- Нормализация и нормальные формы
Реляционная модель данных основана на понятии отношения, которое пришло из теории множеств.
- Множество
- Кортеж
- Декартово произведение множеств
- Отношение в теории множеств
- Операция в теории множеств
Множество - это неупорядоченный набор уникальных объектов, обладающих схожими признаками. Эти объекты называются элементами множества.
Примеры множеств: множество женских имён: N = { Ася, Сара, Рози }, множество профессий: J = { дизайнер, программист }, множество дат: D = { 2019, 2020 }.
Неупорядоченность множества означает, что множества { 2019, 2020 } и { 2020, 2019 } считаются одинаковыми.
Уникальность элементов множества означает, что множество не может содержать двух и более одинаковых элементов. То есть набор { 3, 2, 2 } - не множество.
Кортежем длины n называют упорядоченный набор объектов (не обязательно уникальных) (x1, x2, ..., xn), где x1, x2, ..., xn принадлежат множествам X1, X2, ... Xn (не обязательно различным).
Примеры кортежей: кортеж из элементов множеств N, J, D соответственно: (Ася, дизайнер, 2019), координаты точки на плоскости: (1, 1), набор аргументов функции f(x1, x2, x3).
Упорядоченность кортежа означает, что кортежи (Рози, 2020) и (2020, Рози) считаются различными. Соблюдение порядка элементов кортежа так же важно, как соблюдение порядка букв в словах и цифр в числах.
Кортеж длины 2 также называют упорядоченной парой, кортеж длины 3 - упорядоченной тройкой.
Декартово произведение множеств X1, X2, ..., Xn - это операция над множествами X1, X2, ..., Xn (не обязательно различными), результатом которой является множество всевозможных кортежей (x1, x2, ..., xn), где элементы x1, x2, ..., xn принадлежат множествам X1, X2, ..., Xn соответственно. Обозначение: X1 × X2 × ... × Xn.
Например, декартово произведение N × D = { (Ася, 2019), (Ася, 2020), (Сара, 2019), (Сара, 2020), (Рози, 2019), (Рози, 2020) }, а декартово произведение N × J × D состоит из 3 • 2 • 2 = 12 кортежей по типу (Ася, дизайнер, 2019).
n-арным отношением R между множествами X1, X2, ..., Xn называют подмножество декартового произведения X1 × X2 × ... × Xn, то есть множество, содержащее некоторые элементы (кортежи) множества X1 × X2 × ... × Xn. Число n - арность отношения.
Например, множество { (Сара, 2019), (Рози, 2020) } является бинарным отношением между множествами N и D.
Отношения могут обладать некоторыми свойствами и наделять ими свои элементы.
Примеры известных отношений: меньше (x < y), больше (x > y), равно (x = y), перпендикулярность, параллельность (x || y), x - отец y и так далее.
Пусть имеются множества X и Y, причём множество X является подмножеством декартова произведения n непустых множеств: X = X1 × X2 × … × Xn. Тогда n-арной операцией называется бинарное отношение f ⊆ X × Y, такое, что каждому значению x ∈ X соответствует единственное значение y ∈ Y. Здесь элемент x представляет собой кортеж (x1, x2, …, xn).
Элементы x1, x2, …, xn называют операндами, а элемент y - результатом операции f.
Общее обозначение n-арной операции f: f(x1, x2, ..., xn) = y.
Операция с одним операндом называется унарной операцией, с двумя - бинарной.
Операции обычно имеют своё уникальное символьное обозначение.
Самыми широко используемыми операциями являются арифметические операции. Например, бинарная операция “сложение” (x1 + x2 = y), бинарная операция “возведение в степень” (x1 ^ x2 = y), унарная операция “факториал” (x! = y) и так далее.
Тип данных - это множество допустимых значений. Любое значение (переменной, константы, параметра, атрибута и так далее) принадлежит какому-то типу данных.
В реляционной модели данных тип данных также называют доменом.
Вообще говоря, основными типами данных в программировании являются числовой (7, 3.14), строковый ("развитие", 'engineer') и логический (истина, ложь).
На практике же часто выделяют больше типов, чтобы сузить область допустимых значений и, таким образом, больше ограничить пользователя. Например, числа подразделяют на целые (integer) и дробные (float).
Если мы указываем, что значение принадлежит какому-то типу, то другому типу оно уже принадлежать не может.
Типы, предопределённые в системе, называют базовыми типами. Многие системы позволяют пользователю на основе базовых типов создавать новые типы, называемые в таком случае пользовательскими типами.
В реляционной модели данных обязательным базовым типом является лишь логический (boolean): без него невозможно рассматривать операции над отношениями.
Понятие отношения в реляционной модели данных довольно близко по смыслу к понятию отношения в теории множеств. При этом множествами X1, X2, ..., Xn выступают типы данных T1, T2, ..., Tn, являющиеся множествами по определению.
Ниже рассмотрим несколько видоизменённое математическое определение.
Пусть имеются типы данных T1, T2, ..., Tn (не обязательно различные), тогда n-арным отношением (англ. relation) R называют подмножество декартового произведения T1 × T2 × ... × Tn. Говоря другими словами, отношение - это некоторое множество (набор) кортежей, имеющих одинаковую схему, то есть кортежей (t1, t2, ..., tn), элементы t1, t2, ..., tn которых принадлежат типам данных T1, T2, ..., Tn соответственно.
На деле же отношение реляционной модели данных имеет особую структуру и не является множеством как таковым. Отношение R состоит из заголовка H и тела B. Фактически, его можно рассматривать как упорядоченную пару заголовка и тела: R = (H, B).
Атрибут (лат. attributio - признак) в философии - отличительный, существенный, неотъемлемый признак (черта, свойство) предмета или явления. Атрибуты независимы друг от друга, то есть один атрибут не может повлиять на другой.
В программировании атрибут (англ. attribute) представляет собой свойство некоторого объекта, элемента или файла.
Объект характеризуется и определяется значениями своих атрибутов.
Атрибут объекта обычно состоит из имени атрибута (name) и значения атрибута (value).
Например, “цвет глаз” можно назвать атрибутом человека, тогда “карий”, “зелёный”, “голубой” и “серый” - некоторые из возможных значений атрибута.
В реляционной модели данных некоторый класс объектов представляется отношением и характеризуется конечным множеством атрибутов, а информация (данные) о конкретном объекте хранится в виде набора значений атрибутов. В этом и кроется главное различие между отношением (из реляционной модели данных) и множеством (из теории множеств): множество обычно представляет собой набор наименований объектов, в то время как отношение представляет множество характеристик объектов.
В реляционной модели данных все значения одного атрибута принадлежат одному конкретному типу данных. Например, нельзя, чтобы атрибут имел числовые и строковые значения одновременно.
Фактически, атрибут в реляционной модели данных можно рассматривать как упорядоченную пару (a, t), где a - название атрибута, t - название типа данных, которому принадлежат значения атрибута. В таком случае значение атрибута можно рассматривать как упорядоченную тройку (a, t, v), где v - значение атрибута a типа t.
Говоря простыми словами, заголовок отношения состоит из атрибутов.
Заголовком (схемой) H (англ. header) отношения R называют множество упорядоченных пар (a, t), где a (англ. attribute) - название атрибута, t (англ. type) - название типа данных (не сам тип данных, являющийся множеством), которому принадлежат значения атрибута a.
Таким образом, заголовок n-арного отношения имеет вид: H = { (a1, t1), (a2, t2), ..., (an, tn) }.
Пример заголовка для отношения между множествами N, J, D: H = { (Имя, string), (Профессия, string), (Дата, date) }.
Число атрибутов n называется степенью отношения или арностью отношения.
Отношение с одним атрибутом называют унарным, с двумя — бинарным, с тремя - тернарным и так далее.
Поскольку множество не может содержать одинаковые элементы, а типы данных в заголовке могут повторяться, названия атрибутов должны быть уникальными. Иначе возможна была бы ситуация H = { (Дата, date), (Дата, date) }, что недопустимо для множеств.
Заголовки двух отношений совпадают, если совпадают их количество атрибутов, все названия атрибутов и типы данных атрибутов.
Говоря простыми словами, тело отношения - это множество кортежей, содержащих значения атрибутов.
Итак, заголовок представляет собой множество атрибутов (a, t): H = { (a1, t1), (a2, t2), ..., (an, tn) }.
Значение атрибута должно быть привязано к самому атрибуту, поэтому оно представляется упорядоченной парой ((a, t), v) или, что то же самое, упорядоченной тройкой (a, t, v), где v (англ. value) - значение атрибута a типа t. Но, поскольку название атрибута уникально, а тип данных привязан к нему парой в заголовке отношения, значение атрибута можно представить ещё проще - упорядоченной двойкой (a, v).
Пусть имеется n атрибутов a1, a2, ..., an. Тогда тело B (англ. body) отношения R - это множество кортежей k = ((a1, v1), (a2, v2), ..., (an, vn)): B = { k1, k2, ..., km }.
Количество упорядоченных пар (a, v) в кортеже k должно совпадать с количеством атрибутов (степенью отношения) n.
Число m (число кортежей k) называется кардинальным числом отношения или кардинальностью отношения.
Стоит отметить, что, несмотря на математическое определение кортежа и начальное определение автора, кортеж k не обязательно должен быть упорядочен. Действительно, значения напрямую привязаны к своим атрибутам упорядоченными парами и в случае перестановки ((a2, v2), (a1, v1)) эта связь не нарушается. Более того, сами атрибуты в заголовке не упорядочены по определению множества, а значит упорядоченность значений избыточна. Упорядоченность имела бы смысл, если бы кортежи k имели вид: (v1, v2, ..., vn). Далее при необходимости в упорядоченности, чтобы не путать упорядоченные математические кортежи и неупорядоченные кортежи k, будем использовать понятия “упорядоченный набор”, “упорядоченная пара” и “упорядоченная тройка”.
Пусть имеется заголовок H вида:
H = { (Имя, string), (Профессия, string), (Дата, date) }
Ниже представлен пример тела B, соответствующего заголовку H:
B = {
((Имя, Сара), (Профессия, программист), (Дата, 2019)),
((Имя, Рози), (Профессия, дизайнер), (Дата, 2019)),
}
Графически, упорядоченная пара (a, v) - одна ячейка таблицы, а кортеж k - строка таблицы.
Свойства отношения опираются на его структуру.
Поскольку заголовок H и тело B отношения R являются множествами, их свойства вытекают из свойств множеств:
- Тело отношения
Bне может содержать двух и более одинаковых кортежей. - Порядок следования атрибутов в заголовке
Hотношения не имеет значения. - Порядок следования кортежей в теле
Bотношения не имеет значения.
Отношение удобно представлять в виде таблицы, столбцы которой соответствуют атрибутам, строки - кортежам, а ячейки таблицы (пересечения строк и столбцов) - значениям атрибутов.
Отношение R из последнего примера выше можно представить таблицей следующего вида:
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Сара | программист | 2020 |
Отношения нередко называют таблицами, но это не совсем правильно: таблица лишь отображает структурные элементы отношения в удобном формате, но не отражает сути понятия отношения и его свойства.
Например, понятие таблицы не подразумевает уникальности строк, в то время как в отношении требуется уникальность кортежей.
Для равнозначности понятий таблицы и отношения на понятие таблицы необходимо накладывать некоторые ограничения:
- Все свойства отношений должны быть верны для строк и столбцов таблицы.
- Таблица не должна содержать дополнительных возможностей, выходящих за рамки понятия отношения. (например, скрытые поля с порядком строк и скрытые методы их сортировки, встроенные в строки даты и так далее).
- Об идентификации
- Потенциальный ключ
- Первичный ключ
- Простые и составные ключи
- Значение ключа
- Естественный ключ
- Суррогатный ключ
- Внешний ключ
Идентификатор, ID (англ. identifier — опознаватель) — это уникальный признак объекта.
Идентификаторы позволяют идентифицировать объект, то есть выделить (найти) его среди других объектов.
Например, можно по номеру паспорта определить (идентифицировать) человека, по IP-адресу - физический компьютер, по координатам - точку на карте, по штрих-коду - товар в магазине, по QR-коду - ссылку, по ссылке - сайт и так далее.
Для идентификации кортежей отношения используются ключи.
Как мы уже знаем, каждый кортеж отношения уникален. Поэтому при необходимости найти конкретный кортеж операцией выборки, достаточно перечислить все значения его атрибутов, но обычно так не делают.
Пусть имеется отношение R, содержащее в заголовке n атрибутов.
Потенциальным ключом (англ. candidate key) называют подмножество из m <= n атрибутов отношения R, которое удовлетворяет условиям уникальности и минимальности.
Условие уникальности требует, чтобы в отношении R не могло существовать двух кортежей, содержащих одни и те же значения тех атрибутов, из которых состоит потенциальный ключ.
Важно отметить, что R именно “не может содержать двух кортежей”, а не “не содержит двух кортежей”. Например, если в компании работает несколько сотрудников и все имеют различный цвет глаз, это не означает, что цвет глаз является уникальным идентификатором: всегда может появиться новый сотрудник с уже существующим цветом глаз и идентификация станет невозможной.
Таким образом, уникальность опирается не на наличие объекта в текущий момент времени, а на особенности (природу) класса объектов, которые допускают возможность появления нового объекта в любой момент времени. Неплохими идентификаторами сотрудника компании могут стать: серия и номер паспорта, номер трудовой книги, рабочий email или номер телефона (все они не могут повториться у двух сотрудников).
Условие минимальности (несократимости) требует, чтобы среди атрибутов потенциального ключа отсутствовало меньшее подмножество из k <= m атрибутов, которое удовлетворяет условию уникальности.
Другими словами, при исключении любых атрибутов из потенциального ключа условие уникальности должно перестать выполняться.
В отношении всегда существует хотя бы один потенциальный ключ (множество всех значений атрибутов кортежа), поскольку все кортежи отношения уникальны по определению.
В отношении может одновременно существовать несколько потенциальных ключей.
На примере ниже представлено отношение с двумя потенциальными ключами, каждый из которых состоит из одного элемента: { Номер телефона } и { Email }.
| Имя | Фамилия | Номер телефона | |
|---|---|---|---|
| Джон | Дрим | 8768 | johndream@example |
| Фрэнк | Старк | 1442 | frankstark@example |
| Ален | Стоун | 3567 | alenstone@example |
Как уже отмечалось ранее, отношение может содержать несколько потенциальных ключей, по каждому из которых можно идентифицировать объект. Один из этих ключей выбирается в качестве основного и называется первичным ключом (англ. primary key) отношения, оставшиеся потенциальные ключи называют альтернативными ключами отношения.
Если в отношении содержится только один потенциальный ключ, то он и является первичным ключом данного отношения.
Если в отношении содержится несколько потенциальных ключей, то имеется два основных критерия выбора первичного ключа:
- Удобство использования. Чаще всего выбирается потенциальный ключ с наименьшим физическим размером (занимает меньше памяти на компьютере) или содержит наименьшее количество атрибутов.
- Сохранение уникальности с течением времени. Некоторые идентификаторы могут утрачивать свою уникальность со временем.
Первичный ключ, состоящий из одного атрибута, называют простым ключом, а состоящий из нескольких атрибутов - составным ключом.
Чаще всего используются простые ключи (id, number, username, email, phone), поскольку их проще хранить, с ними проще работать.
Редко, но бывают случаи, когда удобнее использовать составной ключ или нет возможности использовать простой ключ.
Рассмотрим пример составного ключа, разбив телефонный номер на группы. В системе, работающей с телефонными номерами, разбиение номера могло бы оптимизировать поиск и фильтрацию. Из чего состоит телефонный номер? Его схема различна в разных странах. Например, американский номер состоит из кода страны, кода региона, кода телефонного узла и абонентского номера: +1 (234) 235 1779.
| Код региона | Код узла | Абонентский номер | Фамилия владельца |
|---|---|---|---|
| 234 | 145 | 1984 | Ирвинг |
| 234 | 310 | 7817 | Купер |
| 235 | 420 | 6168 | Редли |
Тогда первичный ключ PK отношения R имеет вид:
{
(Код региона, integer),
(Код узла, integer),
(Абонентский номер, integer)
}
Исключение любых атрибутов из ключа PK приведёт к утрате уникальности этого ключа.
Другие примеры составных ключей: серия и номер паспорта (не уникальны по-отдельности), масть и число на игровой карте, номер ряда и номер места в кинотеатре, составные части URL, составные части адреса электронной почты и так далее.
Значением ключа называют кортеж, состоящий из значений атрибутов этого ключа.
| Номер ряда | Номер места | Место для влюблённых | VIP |
|---|---|---|---|
| 7 | 18 | true | false |
| 7 | 19 | true | false |
| 3 | 22 | false | false |
| 10 | 3 | false | true |
Тогда первичный ключ PK отношения R имеет вид:
{
(Номер ряда, integer),
(Номер места, integer)
}
Кортеж ((Номер ряда, 7), (Номер места, 18)) - одно из доступных значений ключа PK.
Разделим понятия реального объекта (дом, машина, человек, дерево) и его физического представления в некоторой базе данных (например, набор значений атрибутов).
Естественный ключ (англ. natural key, business key, domain key) - это уникальный ключ, который идентифицирует некоторый реальный объект, при этом полностью зависит от свойств (природы) этого объекта и не зависит от его физического представления (реализации) в конкретной базе данных.
Естественный ключ подбирается на основе реальных наблюдений за объектом, поэтому его уникальность опирается исключительно на уникальные черты реального объекта.
В рамках реляционной модели данных природа объекта описывается с помощью атрибутов, а поскольку потенциальный ключ напрямую зависит от них, он и является естественным ключом.
Как и в случае с потенциальными ключами, естественных ключей у отношения может быть несколько.
Примеры естественных ключей всё те же, что и примеры потенциальных ключей: человека можно идентифицировать по серии и номеру паспорта, пользователя - по email, дом - по адресу и так далее. Всё это - естественные ключи, взятые из предметных областей (доменов) соответствующих объектов.
Естественные ключи являются полноценной частью приложения и не скрываются от глаз пользователя (например, на сайтах можно часто видеть email или номер телефона).
Суррогатный ключ (англ. surrogate key, pseudokey, synthetic key) - это уникальный идентификатор как для реального объекта, так и для его физического представления в базе данных.
В отличие от естественного ключа, суррогатный ключ никак не зависит от свойств реального объекта, а следовательно и от атрибутов. Суррогатный ключ генерируется системой по заданному алгоритму, поэтому не имеет смысла вне системы.
Чаще всего суррогатный ключ представляет собой некоторый хэш (произвольно сгенерированная строка) или целое число.
Самые распространённые способы генерации суррогатного ключа:
- Universally Unique Identifier (UUID). Пример:
9fc2e9ad-fae7-4f25-ae48-3f45284f2299. - Globally Unique Identifier (GUID). Пример:
75f067fa-b95f-405c-a1bd-29164461a65f. - Object Identifier (ObjectID). Пример:
ObjectId("507f1f77bcf86cd899439017"). AUTO_INCREMENT(MySQL),AUTOINCREMENT(SQLite),SEQUENCE(SQL Server, Oracle).
Суррогатные ключи относятся к внутренней логике (внутренней реализации) приложения. Они не несут в себе никакой полезной информации для пользователя и поэтому скрыты от его глаз.
У отношения может быть несколько суррогатных ключей.
Преимущества использования суррогатных ключей:
- Неизменность (стабильность). Благодаря независимости суррогатного ключа от атрибутов объекта, ключ остаётся неизменным при изменении этого объекта.
- Компактность. Суррогатный ключ всегда состоит только из одного значения, размер которого фиксирован и обычно невелик (размер ключа зависит от алгоритма генерации, который можно выбирать), что уменьшает расход памяти и увеличивает скорость поиска объектов по ключу.
- Единообразие. Если все ключи в системе создаются по одному алгоритму (например, везде используется UUID), то логика обработки данных упрощается и становится предсказуемой для разработчика в любом месте системы.
- Уникальность ключа во всей системе - не только в отдельном отношении.
Основным недостатком суррогатного ключа является то, что он не связан с самим объектом по смыслу: по такому ключу нельзя судить как о содержимом объекта, так и о классе, которому этот объект принадлежит.
Внешние ключи подобны ссылкам на объекты. Внешний ключ позволяет в кортеже одного отношения сослаться на кортеж другого отношения, поскольку хранит значение потенциального ключа этого отношения.
Использование внешних ключей лишает нас необходимости в дублировании данных. На эти данные можно просто сослаться из другого места. Такой подход
Пусть имеются отношения R и S (не обязательно различные). Внешним ключом (англ. foreign key) FK называют такое подмножество атрибутов отношения S, для которого выполняются условия:
- Отношение
Rимеет такой потенциальный ключCK, что у ключейCKиFKсовпадают количество атрибутов и их типы данных (при переименовании атрибутов ключи совпадают). - Каждое значение внешнего ключа
FKв некотором кортеже отношенияSсовпадает со значением потенциального ключаCKв некотором кортеже отношенияR. Иначе говоря, множество значений ключаFKсодержится (является подмножеством) в множестве значений ключаCK.
Обозначение внешнего ключа FK, ссылающегося на потенциальный ключ CK: FK → CK.
Отношение R, содержащее потенциальный ключ CK, называют главным (родительским) отношением, отношение S, содержащее внешний ключ FK, называют подчинённым (дочерним) отношением.
Ниже представлен пример простенькой реализации чата. Родительское отношение R имеет потенциальный ключ ID, дочернее отношение S имеет два внешних ключа: SENDER_ID → ID (идентификатор отправителя) и RECIPIENT_ID → ID (идентификатор получателя).
| ID | Имя пользователя |
|---|---|
| cde | Сара |
| fkh | Рози |
| ID | Текст | SENDER_ID | RECIPIENT_ID |
|---|---|---|---|
| erksh | Доброй ночи | cde | fkh |
| jhtbe | И тебе | fkh | cde |
- О связях между отношениями
- Один к одному (1:1)
- Один ко многим (1:M)
- Многие к одному (M:1)
- Многие ко многим (M:M)
- Ссылочная целостность
Связи между отношениями похожи по смыслу на бинарные отношения из теории множеств. Связи передают то, как объекты соотносятся (связаны) друг с другом. В терминах реляционной модели данных: связь устанавливает соотношение (соответствие) между кортежами двух отношений, причём не обязательно различных.
Связи между двумя отношениями устанавливаются при помощи внешних ключей.
Как уже ранее отмечалось, отношение, содержащее внешний ключ FK → CK, называют дочерним, а отношение, содержащее потенциальный ключ CK, называют родительским.
Между родительским и дочерним отношениями существует 4 вида связей:
Один к одному 1:1 (англ. one-to-one) - такая связь между отношениями R и S, при которой каждому кортежу отношения R соответствует только один (или ни одного) кортеж отношения S и каждому кортежу отношения S соответствует только один (или ни одного) кортеж отношения R. Другими словами, отношение R имеет потенциальный ключ R.CK и внешний ключ R.FK → S.CK, а отношение S имеет потенциальный ключ S.CK и внешний ключ S.FK → R.CK.
Пример связи 1:1: одно королевство - один король (королева).
Отношение R, представленное таблицей ниже, содержит потенциальный ключ R.ID и внешний ключ R.KINGDOM_ID → S.ID, отношение S содержит потенциальный ключ S.ID и внешний ключ S.KING_ID → R.ID.
| ID | Имя | KINGDOM_ID |
|---|---|---|
| val | Виллем-Александр | NL |
| el2 | Елизавета II | GB |
| k16 | Карл XVI Густав | SE |
| ma2 | Маргрета II | DK |
| fil | Филипп | BE |
| fi6 | Филипп VI | ES |
| ha5 | Харальд V | NO |
| ID | Название | KING_ID |
|---|---|---|
| BE | Бельгия | fil |
| GB | Великобритания | el2 |
| DK | Дания | ma2 |
| ES | Испания | fi6 |
| NL | Нидерланды | val |
| NO | Норвегия | ha5 |
| SE | Швеция | k16 |
Один ко многим 1:M (англ. one-to-many) - такая связь между отношениями R и S, при которой каждому кортежу отношения R соответствует несколько (или ни одного) кортежей отношения S и каждому кортежу отношения S соответствует не более одного кортежа отношения R. Другими словами, отношение R имеет потенциальный ключ R.CK, а отношение S имеет внешний ключ S.FK → R.CK.
Рассмотрим интересный пример связи 1:M: один отец может иметь нескольких детей, но каждый ребёнок имеет лишь одного биологического отца.
Как уже ранее отмечалось, отношения R и S не обязательно различны (они могут представлять собой одно и то же отношение). Отношение R, представленное таблицей ниже, содержит потенциальный ключ R.ID и внешний ключ R.PARENT_ID → R.ID.
| ID | Имя | Возраст | PARENT_ID |
|---|---|---|---|
| P1 | Джон | 65 | NULL |
| P2 | Джим | 43 | P1 |
| P3 | Джек | 20 | P2 |
| P4 | Джоан | 22 | P2 |
Многие к одному M:1 (англ. many-to-one) - зеркальное отражение связи 1:M (в определении достаточно поменять местами отношения R и S).
Пример связи M:1: много городов может принадлежать одной стране, но один город не может принадлежать двум странам одновременно.
Отношение R, представленное таблицей ниже, имеет потенциальный ключ R.ID и внешний ключ R.COUNTRY_ID → S.ID, а отношение S содержит только потенциальный ключ S.ID.
| ID | Имя | COUNTRY_ID |
|---|---|---|
| kiev | Киев | UA |
| lviv | Львов | UA |
| sydney | Сидней | AU |
| ID | Название |
|---|---|
| UA | Украина |
| AU | Австралия |
Многие ко многим M:M (англ. many-to-many) - такая связь между отношениями R и S, при которой каждому кортежу отношения R соответствует несколько (или ни одного) кортежей отношения S и каждому кортежу отношения S соответствует несколько (или ни одного) кортежей отношения R. С помощью двух отношений такую связь на практике реализовать нельзя.
Другими словами, отношение R имеет потенциальный ключ R.CK, отношение S имеет потенциальный ключ S.CK и существует такое отношение T, тело которого состоит из кортежей (T.FK1 → S.CK, T.FK2 → R.CK), содержащих внешние ключи, ссылающиеся на потенциальные ключи отношений R и S. Отношение T имеет связь M:1 (многие к одному) с каждым из отношений R и S.
Пример связи M:M: каждый сотрудник компании может знать несколько разговорных языков, несколько сотрудников могут говорить на одном языке.
| ID | Язык |
|---|---|
| en | Английский |
| ru | Русский |
| fr | Французский |
| ID | Имя | Должность |
|---|---|---|
| sara1 | Сара | программист |
| dina7 | Дина | дизайнер |
Пусть Сара знает английский и французский, а Дина - английский и русский, тогда:
| ID | WORKER_ID | LANGUAGE_ID |
|---|---|---|
| t1 | sara1 | en |
| t2 | sara1 | fr |
| t3 | dina7 | en |
| t4 | dina7 | ru |
В таблице выше можно увидеть, что отношение T имеет внешний ключ T.LANGUAGE_ID → R.ID (связь M:1 между T и R) и внешний ключ T.WORKER_ID → S.ID (связь M:1 между T и S).
Ещё один пример связи M:M: у научной статьи может быть несколько авторов, у автора может быть много научных статей.
Первичные ключи могут изменяться. Простой пример: пользователь хочет сменить email или номер телефона. Если существует связь, использующая подобный потенциальный ключ CK, то ссылающийся на него внешний ключ FK → CK также должен измениться.
Ссылочной целостностью (англ. referential integrity) называют корректность значений всех внешних ключей, то есть хранение актуального значения потенциального ключа CK в каждом внешнем ключе FK → CK.
Чтобы обеспечить ссылочную целостность, необходимо при изменении значения потенциального ключа CK заменить значения всех ссылающихся на него внешних ключей FK или отменить операцию изменения, если замена невозможна.
В некоторых системах управления базами данных ссылочная целостность поддерживается автоматически.
- О реляционных операциях
- Ограничения на применение операций
- Объединение отношений
- Пересечение отношений
- Вычитание отношения
- Декартово произведение отношений
- Проекция отношения
- Выборка (ограничение)
- Соединение
- Деление отношений
- Переименование атрибутов отношения
Большинство операций над отношениями являются бинарными, то есть такими, которые оперируют двумя отношениями. При необходимости произвести бинарную операцию над большим количеством отношений, её необходимо применить последовательно несколько раз: P • R • S • T = ((P • R) • S) • T.
Любая операция над отношениями, результатом которой является отношение, называется реляционной операцией.
Систему реляционных операций называют реляционной алгеброй.
Можно придумать бесконечно много реляционных операций, но абсолютное большинство из них будут выражаться через другие реляционные операции, то есть будут являться результатом последовательного применения нескольких более простых реляционных операций.
Эдгар Кодд, создатель реляционной модели данных, ввёл следующий набор из 8 реляционных операций:
- Объединение отношений
- Пересечение отношений
- Вычитание отношения
- Декартово произведение отношений
- Проекция отношения
- Выборка (ограничение)
- Соединение
- Деление отношений
Операции объединения, пересечения, вычитания и декартова произведения относят к теоретико-множественным операциям: они являются аналогами одноимённых операций над множествами в теории множеств.
Операции проекции, выборки, соединения и деления называют специальными операциями: они имеют смысл только в рамках реляционной модели данных.
Операции объединения, вычитания, проекции, декартова произведения и выборки являются примитивными реляционными операциями — такими операциями, которые нельзя выразить друг через друга.
Далее в примерах отношения будут представляться таблицами для наглядности.
Операции объединения, пересечения и вычитания требуют совпадения заголовков у своих отношений-операндов.
Операции декартова произведения и соединения требуют, чтобы в заголовках их отношений-операндов не было совпадающих названий атрибутов. Если таковые имеются, то их необходимо переименовать до применения операции.
Объединение отношений R и S - бинарная операция, требующая совпадения заголовков отношений R и S, результатом которой является отношение с тем же заголовком и телом, содержащим все кортежи обоих отношений. Иначе говоря, каждый кортеж отношения-результата принадлежит или отношению R, или отношению S. Если отношения R и S имеют между собой совпадающие кортежи (пересечения), то в отношение-результат попадает только один из них.
Обозначение объединения отношений R и S: R UNION S.
Объединение отношений является аналогом объединения множеств из теории множеств.
Ниже представлен пример объединения отношений R и S.
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Сара | программист | 2020 |
| Имя | Профессия | Дата |
|---|---|---|
| Рози | дизайнер | 2020 |
| Сара | программист | 2020 |
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Сара | программист | 2020 |
| Рози | дизайнер | 2020 |
Пересечение отношений R и S - бинарная операция, требующая совпадения заголовков отношений R и S, результатом которой является отношение с тем же заголовком и телом, содержащим только те кортежи, которые содержатся в обоих отношениях. Иначе говоря, каждый кортеж отношения-результата содержится и в отношении R, и в отношении S.
Обозначение пересечения отношений R и S: R INTERSECT S.
Пересечение отношений является аналогом пересечения множеств из теории множеств.
Операция пересечения отношений не является примитивной, поскольку её можно выразить через операцию вычитания отношения: R INTERSECT S = R MINUS (R MINUS S).
Ниже представлен пример пересечения отношений R и S.
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Сара | программист | 2020 |
| Имя | Профессия | Дата |
|---|---|---|
| Рози | дизайнер | 2020 |
| Сара | программист | 2020 |
| Имя | Профессия | Дата |
|---|---|---|
| Сара | программист | 2020 |
Вычитание отношения S из R - бинарная операция, требующая совпадения заголовков отношений R и S, результатом которой является отношение с тем же заголовком и телом, содержащим только те кортежи, которые принадлежат отношению R, но не принадлежат отношению S.
Обозначение вычитания отношения S из R: R MINUS S.
Вычитание отношения является аналогом разности множеств из теории множеств.
Ниже представлен пример вычитания отношения S из R.
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Сара | программист | 2020 |
| Имя | Профессия | Дата |
|---|---|---|
| Рози | дизайнер | 2020 |
| Сара | программист | 2020 |
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
Пусть имеются отношение R с атрибутами a1, a2, ..., an и значениями атрибутов x1, x2, ..., xn в кортежах и отношение S с атрибутами b1, b2, ..., bm и значениями атрибутов y1, y2, ..., ym в кортежах.
Декартово произведение отношений R и S - это бинарная операция, требующая отсутствия совпадающих названий атрибутов между заголовками отношений R и S (между { a1, a2, ..., an } и { b1, b2, ..., bm }), результатом которой является новое отношение T, заголовок которого получается в результате конкатенации (соединения, сцепления) заголовков отношений R и S, а тело состоит из упорядоченных пар ((x1, x2, ..., xn), (y1, y2, ..., ym)), содержащих всевозможные комбинации кортежей отношений R и S соответственно. Для упрощения можно раскрыть скобки внутри упорядоченной пары (порядок элементов сохранится): (x1, x2, ..., xn, y1, y2, ..., ym).
Обозначение декартова произведения R и S: R TIMES S.
Декартово произведение отношений является аналогом декартова произведения множеств из теории множеств.
Ниже представлен пример декартова произведения отношений R и S.
| Имя | Фамилия |
|---|---|
| Ася | Тургенева |
| Рози | Фицджеральд |
| Сара | Фаулз |
| Профессия | Дата |
|---|---|
| дизайнер | 2019 |
| программист | 2020 |
Количество атрибутов в отношении R TIMES S: n(R) + n(S) = 2 + 2 = 4, количество кортежей: m(R) • m(S) = 3 • 2 = 6.
| Имя | Фамилия | Профессия | Дата |
|---|---|---|---|
| Ася | Тургенева | дизайнер | 2019 |
| Ася | Тургенева | программист | 2020 |
| Рози | Фицджеральд | дизайнер | 2019 |
| Рози | Фицджеральд | программист | 2020 |
| Сара | Фаулз | дизайнер | 2019 |
| Сара | Фаулз | программист | 2020 |
Проекция отношения R - унарная операция над отношением R, которая позволяет составить новое отношение из выбранных атрибутов отношения R и их значений в кортежах отношения R.
Графически проекция отношения R - это выборка столбцов таблицы.
Если при проецировании возникают кортежи-дубликаты, то они удаляются из результата.
Если отношение R имеет n атрибутов, то его можно обозначить: R(a1, a2, ..., an). Тогда проекция, включающая в себя первый и третий атрибуты отношения R обозначается: PROJECT R { a1, a3 } или R[a1, a3].
Ниже представлен пример проекции отношения R.
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Рози | дизайнер | 2019 |
| Сара | программист | 2020 |
| Рози | дизайнер | 2020 |
| Имя | Профессия |
|---|---|
| Ася | дизайнер |
| Рози | дизайнер |
| Сара | программист |
Перечёркнутая строка является дубликатом второй строки, поэтому она не попадает в результат.
Выборка из (ограничение, селекция) отношения R - унарная операция над отношением R, результатом которой является отношение с тем же заголовком, что и у R, содержащее в своём теле те кортежи из R, которые удовлетворяют некоторому условию c.
Условие c задаётся логическим выражением (его результатом являются истина или ложь), которое может содержать названия атрибутов и константы (числа, строки, даты и так далее). Если результат выполнения выражения c - истина, то кортеж попадает в результат выборки, иначе - не попадает.
Обозначение выборки из отношения R: R WHERE c.
Ниже представлен пример выборки из отношения R.
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Сара | программист | 2020 |
| Рози | дизайнер | 2020 |
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Рози | дизайнер | 2020 |
| Имя | Профессия | Дата |
|---|---|---|
| Сара | программист | 2020 |
| Рози | дизайнер | 2020 |
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Сара | программист | 2020 |
Соединение отношений R и S - бинарная операция, результатом которой является результат последовательного применения операций декартового произведения R TIMES S и выборки из получившегося отношения с условием c.
Как и в случае с декартовым произведением, в заголовках отношений R и S не должно быть совпадающих атрибутов. Если такие имеются, то их необходимо переименовать перед тем, как производить операцию соединения.
Обозначение соединения отношений R и S: (R TIMES S) WHERE c.
Одним из самых простых соединений является то соединение, в условии которого используются константа (например, число или строка) и один атрибут одного из отношений-операндов соединения. Такое соединение отношений R и S представлено на примере ниже.
| Имя | Фамилия |
|---|---|
| Ася | Тургенева |
| Рози | Фицджеральд |
| Сара | Фаулз |
| Профессия | Дата |
|---|---|
| дизайнер | 2019 |
| программист | 2020 |
| Имя | Фамилия | Профессия | Дата |
|---|---|---|---|
| Ася | Тургенева | дизайнер | 2019 |
| Ася | Тургенева | программист | 2020 |
| Рози | Фицджеральд | дизайнер | 2019 |
| Рози | Фицджеральд | программист | 2020 |
| Сара | Фаулз | дизайнер | 2019 |
| Сара | Фаулз | программист | 2020 |
| Имя | Фамилия | Профессия | Дата |
|---|---|---|---|
| Ася | Тургенева | дизайнер | 2019 |
| Рози | Фицджеральд | дизайнер | 2019 |
| Сара | Фаулз | дизайнер | 2019 |
Пусть имеются отношение R с заголовком { a1, a2, ..., an, b1, b2, ... bm } и телом, содержащим множество кортежей вида (x1, x2, ... xn, y1, y2, ... ym), и отношение S с заголовком { b1, b2, ... bm } и телом, содержащим множество кортежей вида (y1, y2, ... ym).
Делением отношения R на отношение S называют бинарную операцию над отношениями R и S, результатом которой является отношение T с заголовком { a1, a2, …, an } и телом, содержащим множество таких кортежей (x1, x2, …, xn), что для каждого кортежа (y1, y2, …, ym) отношения S в R существует кортеж (x1, x2, …, xn, y1, y2, …, ym). В этом случае отношение R называют делимым, отношение S - делителем, отношение T - частным.
Другими словами, в результат деления отношения R на отношение S попадают такие кортежи x = (x1, x2, ... xn) делимого R, c которыми каждый кортеж y = (y1, y2, …, ym) делителя S (то есть все кортежи отношения S без исключения) состоит в упорядоченной паре (x, y), или, что то же самое, в кортеже (x1, x2, ..., xn, y1, y2, ..., ym), принадлежащем отношению R.
Обозначение деления отношения R на S: R DIVIDEBY S.
Чем деление отношений похоже на деление чисел? При делении чисел x ÷ y мы считаем, сколько раз делитель y целиком поместится в делимое x. Пример: 7 ÷ 3 = 3 + 3 + 1 = 2 полных раза. При делении отношений в результат попадает каждый кортеж x, который связан упорядоченными парами со всеми без исключения кортежами y, заданными в отношении-делителе.
Деление отношений проще понять на примере. Ниже представлен пример деления отношения R на отношение S.
| Имя | Работа | Разговорный язык |
|---|---|---|
| Ася | дизайнер | русский |
| Ася | дизайнер | немецкий |
| Сара | программист | английский |
| Сара | программист | французский |
| Дина | программист | русский |
| Дина | программист | итальянский |
| Дина | программист | английский |
| Рози | дизайнер | русский |
| Рози | дизайнер | английский |
| Разговорный язык |
|---|
| русский |
| английский |
Кто из сотрудников компании в отношении R знает все перечисленные в отношении S языки?
| Имя | Работа |
|---|---|
| Сара | программист |
| Рози | дизайнер |
Сара и Рози знают все перечисленные языки.
Кортежи x = (x1, x2) = (Сара, программист) и x = (x1, x2) = (Рози, дизайнер) попали в результат, поскольку они состоят в упорядоченных парах с каждым из кортежей y = (y1) = (русский), y = (y1) = (английский) отношения S.
В отличие от остальных операций, предложенных Эдгаром Коддом, деление отношений не обрело широкой популярности и используется крайне редко. Такой же результат можно получить комбинацией операций выборки и проекции - такой подход будет гораздо гибче.
Переименование атрибутов отношения R - унарная операция над отношением R, результатом которой является отношение с телом отношения R и изменёнными названиями указанных атрибутов в заголовке.
Результатом применения операции переименования атрибутов является отношение с изменёнными именами атрибутов.
Обозначение переименования атрибутов { a1, a2, ..., am } отношения R на новые имена { b1, b2, ..., bm }: R RENAME a1, a2, …, am AS b1, b2, …, bm.
| Имя | Профессия | Дата |
|---|---|---|
| Ася | дизайнер | 2019 |
| Сара | программист | 2020 |
| Рози | дизайнер | 2020 |
| Имя | Специальность | Год |
|---|---|---|
| Ася | дизайнер | 2019 |
| Сара | программист | 2020 |
| Рози | дизайнер | 2020 |
В зависимости от вида условия c различают несколько разновидностей соединений.
Пусть a - один из атрибутов отношения R, b - один из атрибутов отношения S.
Тэта-соединением (Θ-соединением) отношения R по атрибуту a с отношением S по атрибуту b называют соединение отношений R и S с условием c вида a Θ b, где символ Θ является одним из следующих символов сравнения { <, >, ≤ (<=), ≥ (>=), =, ≠ (!=) }.
Обозначение Θ-соединения отношения R по атрибуту a с отношением S по атрибуту b: (R TIMES S) WHERE a Θ b, или более кратко R(a Θ b)S.
Рассмотрим Θ-соединение на примере продажи и покупки некоторого предмета, имеющего в качестве характеристик цвет и качество материала. Пусть имеются отношение R, содержащее запросы на покупку предмета, и отношение S, содержащее выставленные на продажу предметы.
| Покупатель | Желаемая цена |
|---|---|
| magic | 240 |
| woodruf | 180 |
| Продавец | Предлагаемая цена | Качество материала | Цвет |
|---|---|---|---|
| evergreen | 230 | 0.7 | зелёный |
| funnyrabbit | 180 | 0.5 | бирюзовый |
| redhot | 300 | 1.0 | красный |
Найдём все соответствия между запросами на покупку предметов и выставленными на продажу предметами при условии, что желаемая цена покупателя выше (или равна) установленной цены продавца.
| Покупатель | Желаемая цена | Продавец | Предлагаемая цена | Качество материала | Цвет |
|---|---|---|---|---|---|
| magic | 240 | evergreen | 230 | 0.7 | зелёный |
| magic | 240 | funnyrabbit | 180 | 0.5 | бирюзовый |
| woodruf | 180 | funnyrabbit | 180 | 0.5 | бирюзовый |
Итак, пользователь magic по своему запросу может купить предмет у пользователя evergreen или у funnyrabbit, а пользователь woodruf - только у funnyrabbit. Пользователь redhot остался без потенциальных покупателей, поскольку установил слишком высокую цену.
Экви-соединением отношения R по атрибуту a с отношением S по атрибуту b называют тэта-соединение, при котором символ Θ является символом равенства =.
Обозначение экви-соединения отношения R по атрибуту a с отношением S по атрибуту b: (R TIMES S) WHERE a = b, или более кратко R(a = b)S.
Рассмотрим экви-соединение на примере отношений R и S, имеющих связь 1:1 и представленных таблицами ниже.
| ID | Имя | KINGDOM_ID |
|---|---|---|
| val | Виллем-Александр | NL |
| el2 | Елизавета II | GB |
| k16 | Карл XVI Густав | SE |
| ma2 | Маргрета II | DK |
| fil | Филипп | BE |
| fi6 | Филипп VI | ES |
| ha5 | Харальд V | NO |
| ID | Название | KING_ID |
|---|---|---|
| BE | Бельгия | fil |
| GB | Великобритания | el2 |
| DK | Дания | ma2 |
| ES | Испания | fi6 |
| NL | Нидерланды | val |
| NO | Норвегия | ha5 |
| SE | Швеция | k16 |
Операцией экви-соединения получим новое отношение, содержащее и страны, и их королей одновременно.
Благодаря тому, что между R и S установлена связь 1:1 (взаимооднозначное соответствие), можем использовать одно из двух условий c (результат будет тот же):
R.KINGDOM_ID = S.ID.S.KING_ID = R.ID.
| R.ID | Имя | KINGDOM_ID | S.ID | Название | KING_ID |
|---|---|---|---|---|---|
| val | Виллем-Александр | NL | NL | Нидерланды | val |
| el2 | Елизавета II | GB | GB | Великобритания | el2 |
| k16 | Карл XVI Густав | SE | SE | Швеция | k16 |
| ma2 | Маргрета II | DK | DK | Дания | ma2 |
| fil | Филипп | BE | BE | Бельгия | fil |
| fi6 | Филипп VI | ES | ES | Испания | fi6 |
| ha5 | Харальд V | NO | NO | Норвегия | ha5 |
Понятно, что получившееся отношение имеет избыточные атрибуты. После применения операций проекции и переименования атрибутов получим новое отношение P:
| ID | Имя правителя | Название страны |
|---|---|---|
| NL | Виллем-Александр | Нидерланды |
| GB | Елизавета II | Великобритания |
| SE | Карл XVI Густав | Швеция |
| DK | Маргрета II | Дания |
| BE | Филипп | Бельгия |
| ES | Филипп VI | Испания |
| NO | Харальд V | Норвегия |
Атрибуты в экви-соединении не обязательно должны быть ключами, но в этом виде соединения ключи в качестве атрибутов используются чаще всего.
Естественное соединение является особым видом соединения, при котором наличие одноимённых атрибутов между отношениями не только разрешено, но и является принципиально важным.
Пусть имеются отношение R с заголовком { a1, a2, ..., an, b1, b2, ... bk } и телом, содержащим кортежи вида (x1, x2, ..., xn, y1, y2, ..., yk), и отношение S с заголовком { b1, b2, ..., bk, c1, c2, ..., cm } и телом, содержащим кортежи вида (y1, y2, ..., yk, z1, z2, ..., zm). Атрибуты b1, b2, ..., bk отношений R и S совпадают (то есть совпадают их имена и типы).
Тогда естественным соединением отношений R и S называют соединение T, имеющее заголовок { a1, a2, ..., an, b1, b2, ... bk, c1, c2, ..., cm }, и тело, содержащее кортежи вида (x1, x2, ..., xn, y1, y2, ..., yk, z1, z2, ..., zm).
Таким образом, при естественном соединении кортежи вида (x1, x2, ..., xn, y1, y2, ..., yk) и (y1, y2, ..., yk, z1, z2, ..., zm) объединяются в один кортеж при совпадении между ними значений y1, y2, ..., yk.
Обозначение естественного соединения отношений R и S: R JOIN S.
| Название | Количество | Номер товара |
|---|---|---|
| Стол | 16 | 501 |
| Стул | 64 | 502 |
| Шкаф | 8 | 503 |
| Цена | Номер товара |
|---|---|
| 1000 | 502 |
| 2000 | 501 |
| 5000 | 503 |
Общим атрибутом отношений G и P является только Номер товара.
| Название | Количество | Цена | Номер товара |
|---|---|---|---|
| Стол | 16 | 2000 | 501 |
| Стул | 64 | 1000 | 502 |
| Шкаф | 8 | 5000 | 503 |
Рекомендуется сперва прочесть: «Свойства бинарных операций (теория множеств)».
Обозначим символом ◇ произвольную реляционную операцию, тогда при изучении свойств обозначение ◇ ∈ { INTERSECT, UNION } будет означать, что свойство выполняется для операций пересечения и объединения.
- Идемпотентность унарных операций:
◇ R = ◇ (◇ R), ◇ ∈ { PROJECT, WHERE }. То естьR[a] = (R[a])[a]иR WHERE c = (R WHERE c) WHERE c. - Идемпотентность бинарных операций:
R ◇ R = R , ◇ ∈ { UNION, INTERSECT }. - Коммутативность:
R ◇ S = S ◇ R , ◇ ∈ { TIMES, JOIN, UNION, INTERSECT }. То естьR TIMES S = S TIMES R. - Ассоциативность:
(R ◇ S) ◇ T = R ◇ (S ◇ T) , ◇ ∈ { TIMES, JOIN, UNION, INTERSECT }. - Дистрибутивность
- проекции относительно пересечения, объединения, разности и декартова произведения:
(R ◇ S)[a] = R[a] ◇ S[a], ◇ ∈ { INTERSECT, UNION, MINUS, TIMES }. - выборки относительно объединения и декартова произведения:
(R ◇ S) WHERE c = (R WHERE c) ◇ (S WHERE c), ◇ ∈ { UNION, TIMES }.
- Выборку
R WHERE cи проекциюR[a]можно менять местами при условии, чтоcзависит только от выбранных атрибутовa:(R WHERE c)[a] = R[a] WHERE c.
Важно отметить, что декартово произведение отношений коммутативно и ассоциативно в отличие от декартова произведения множеств. Это связано с тем, что кортежи отношения не должны быть упорядоченными.
Операция переименования атрибутов в общем случае не идемпотентна.
Функциональной зависимостью (англ. functional dependency) называют бинарное отношение между двумя подмножествами атрибутов некоторого отношения R.
Фактически, функциональная зависимость - это бинарное отношение между двумя ключами (англ. constraint between two keys) в некотором отношении R. Обычно функциональная зависимость отражает связь "один-ко-многим" (1:M).
Пусть у нас имеются два подмножества атрибутов A и B отношения R. Говорят, что множество B функционально зависит (англ. A functionally determine B) от множества A тогда и только тогда, когда каждое значение множества A связано в точности с одним значением множества B.
Говоря другими словами, если два кортежа отношения R совпадают по значениям в подмножестве атрибутов A, то эти кортежи также должны совпадать по значениям и в подмножестве атрибутов B.
Функциональную зависимость B от A обозначают: A → B, при этом подмножество атрибутов A называют детерминантом (англ. determinant set), а подмножество атрибутов B называют зависимой частью (англ. dependent set).
Функциональную зависимость называют тривиальной (англ. trivial), если B ⊆ A, то есть если зависимая часть является подмножеством детерминанта.
| ID | Дата поступления | Название проекта |
|---|---|---|
| ID1001 | 2019 | Jest |
| ID1001 | 2020 | Graphql Apollo |
| ID7017 | 2020 | Graphql Apollo |
| ID7017 | 2021 | Koa |
Нормальной формой (англ. normal form) называют некоторое требование или совокупность требований, которым должно удовлетворять отношение для устранения избыточности.
Если эти требования выполняются для каждого отношения некоторой базы данных, то её называют нормализованной базой данных (англ. normalized database). Сам процесс приведения базы данных к нормализованному виду называется нормализацией этой базы данных (англ. database normalization).
Нормализация позволяет избежать избыточности данных, то есть исключить дублирование данных. Дублирование данных в свою очередь может стать причиной ошибок при изменении данных.
Понятие нормализации ввёл Эдгар Кодд как часть своей реляционной модели.
Денормализацией базы данных (англ. database denormalization) называют процесс, обратный нормализации. В этом случае в базу данных умышленно добавляют избыточные данные (дубликаты данных) для ускорения получения данных.
Когда отношений слишком много и они тесно связаны друг с другом, группировка (операция соединения) данных может занимать продолжительное время (скажем, сделать соединение INNER JOIN десяти таблиц), что плохо сказывается на производительности системы, а значит и на пользовательском опыте.
По этой причине данные иногда умышленно хранят в денормализованном виде, когда одно отношение представляет собой композицию нескольких отношений.
Денормализованный вид усложняет операции создания, обновления и удаления данных (ведь нужно изменять не только сами данные, но и все их дубликаты), а также увеличивает размер базы данных. Зато существенно снижается время отклика системы при чтении данных.
Ниже представлен пример денормализации в табличном виде.
| ID | Заголовок | Текст | Автор |
|---|---|---|---|
N1 |
Нормализация | Нормализацией базы данных называют.... | ID: U1 Никнейм: @max.starling Почта: max.starling@email |
N2 |
Денормализация | Денормализацией базы данных называют.... | ID: U1 Никнейм: @max.starling Почта: max.starling@email |
N3 |
Реляционная модель данных | Реляционной моделью данных называют.... | ID: U2 Никнейм: @ray.gray Почта: ray.gray@email |
Пользователь (хранит идентификаторы публикаций, чтобы публикации было проще найти на странице пользователя)
| ID | Никнейм | Почта | Возраст | Публикации |
|---|---|---|---|---|
U1 |
@max.starling | max.starling@email | 23 | [N1, N2] |
U2 |
@ray.gray | ray.gray@email | 27 | [N3] |
Ниже представлен пример денормализации в документном виде.
[{
"_id": "N1",
"title": "Нормализация",
"text": "Нормализацией базы данных называют....",
"user": {
"_id": "U1",
"username": "@max.starling",
"email": "max.starling@email"
}
},
{
"_id": "N2",
"title": "Денормализация",
"text": "Денормализацией базы данных называют....",
"user": {
"_id": "U1",
"username": "@max.starling",
"email": "max.starling@email"
}
},
{
"_id": "N3",
"title": "Реляционная модель данных",
"text": "Реляционной моделью данных называют....",
"user": {
"_id": "U2",
"username": "@ray.gray",
"email": "ray.gray@email"
}
}][{
"_id": "U1",
"username": "@max.starling",
"email": "max.starling@email",
"age": 23,
"posts": ["N1", "N2"]
}, {
"_id": "U2",
"username": "@ray.gray",
"email": "ray.gray@email",
"age": 27,
"posts": ["N3"]
}]Отношение находится в первой нормальной форме (англ. first normal form), 1НФ (англ. 1NF) тогда и только тогда, когда любой кортеж отношения может содержать только одно значение для каждого из атрибутов.
Таким образом, значение атрибута не может представлять собой набор значений (массив, множество, таблицу и так далее), если отношение находится в 1НФ.
В реляционной модели данных отношение всегда находится в 1НФ по своему определению.
Тем не менее, некоторые таблицы, которыми можно представить отношение, могут не удовлетворять данному требованию, а значит и реляционной модели.
Например, SQL не поддерживает использование столбцов с табличными значениями (англ. table-valued columns).
Пример ненормализованной таблицы:
| Страна | Город |
|---|---|
| Россия | Москва, Санкт-Петербург |
| Украина | Киев, Львов |
| Беларусь | Минск |
Пример нормализованной таблицы:
| Страна | Город |
|---|---|
| Россия | Москва |
| Россия | Санкт-Петербург |
| Украина | Киев |
| Украина | Львов |
| Беларусь | Минск |
Базы данных, которые нарушают первую нормальную форму, обычно называют NoSQL базами данных. Примером такой базы данных является документная база данных MongoDB, которая позволяет хранить любые JSON-документы (значения могут быть массивами или объектами).
N+1 проблема возникает, когда для получения связанных данных делается 1 запрос на основную сущность и n отдельных запросов на связанные сущности.
Рассмотрим эту проблему на примере GraphQL и Prisma.
Например, у нас есть users и posts в отношении 1:M (один-ко-многим), мы хотим вытянуть данные о пользователях и их постах.
При нативной реализации такого запроса на SQL мы сначала возьмём пользователей
SELECT * FROM usersа затем для каждого получаем посты по айди пользователя (n дополнительных запросов):
SELECT * FROM posts WHERE user_id = 1
SELECT * FROM posts WHERE user_id = 2
...
SELECT * FROM posts WHERE user_i = nОтсюда и n + 1 запросов, что неэффективно.
Проблема решается объединением запросов при помощи JOIN. Например, на чистом SQL решение было бы следующим:
SELECT * FROM users u LEFT JOIN posts p WHERE p.user_id = u.idИтого у нас лишь один запрос к базе вместо n + 1.
Допустим, у нас есть следующая схема Prisma.
model User {
id Int @id @default(autoincrement())
name String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
user User @relation(fields: [userId], references: [id])
userId Int
}Тогда проблема n + 1 выглядела бы так на языке JavaScript
const users = await prisma.user.findMany(); // 1 запрос, чтобы получить всех пользователей
const usersWithPosts = await Promise.all(
users.map(async (user) => {
const posts = await prisma.post.findMany({ where: { userId: user.id } }); // n запросов, по одному для каждого пользователя
return { ...user, posts };
})
);Решение проблемы - жадная загрузка (англ. eager loading), подробнее о ней будет ещё далее.
- через
include
const usersWithPosts = await prisma.user.findMany({
include: {
posts: true, // загружаем связанные посты сразу
},
});- через
select
const usersWithPosts = await prisma.user.findMany({
select: {
id: true,
name: true,
posts: {
select: {
id: true,
title: true,
},
},
},
});Пример проблемы n + 1 запроса в GraphQL
query {
users {
id
name
posts {
id
title
}
}
}const resolvers = {
Query: {
users: () => prisma.user.findMany(), // 1 запрос
},
User: {
posts: (parent) => prisma.post.findMany({ where: { userId: parent.id } }), // дополнительные n запросов
},
};Решение
- через
includeв корневом запросе (если ORM предоставляет такую возможность)
const resolvers = {
Query: {
users: () => prisma.user.findMany({ include: { posts: true } }), // ✔️ Один запрос
},
};тогда GraphQL схема будет
type User {
id: ID!
name: String!
posts: [Post!]!
}- через
DataLoader
const postLoader = new DataLoader(async (userIds) => {
const posts = await prisma.post.findMany({
where: { userId: { in: userIds } },
});
// Группируем посты по userId
return userIds.map(userId => posts.filter(post => post.userId === userId));
});const resolvers = {
User: {
posts: (parent) => postLoader.load(parent.id), // одно обращение к DataLoader вместо n запросов
},
};Оставляем задачу с пользователями и постами, имеем 1 запрос для пользователей и n запросов постов для каждого из n пользователей.
const users = await db.collection('users').find().toArray(); // 1 запрос
const usersWithPosts = await Promise.all(
users.map(async (user) => {
const posts = await db.collection('posts').find({ user_id: user._id }).toArray(); // n запросов
return { ...user, posts };
})
);Почему плохо:
- большая нагрузка на сервер (много запросов)
- медленное выполнение
- бОльшие траты из-за бОльшего количества облачных вычислений
Решение:
- Денормализация. Хранить вложенные объекты (объекты постов вложены в объекты пользователей), не вынося их в разные коллекции.
{
"_id": "123",
"name": "Alice",
"posts": [
{ "title": "Post 1" },
{ "title": "Post 2" }
]
}- Агрегации и
$lookup(АналогJOINв MongoDB)
const usersWithPosts = await db.collection('users').aggregate([
{
$lookup: {
from: "posts",
localField: "_id",
foreignField: "user_id",
as: "posts"
}
}
]).toArray();- Запрос
$in. Передаём список айдишек пользователей и имеем1 + 1запрос вместоn + 1:
const users = await db.collection('users').find().toArray();
const userIds = users.map(u => u._id);
const posts = await db.collection('posts').find({ user_id: { $in: userIds } }).toArray();
Опять же, пользователи и посты.
Получаем пользователей.
GET /users/_search
{
"query": {
"match_all": {}
}
}Получаем посты для каждого из пользователей, n пользователей = n дополнительных запросов
GET /posts/_search
{
"query": {
"term": { "user_id": "1" }
}
}...
GET /posts/_search
{
"query": {
"term": { "user_id": "n" }
}
}Решения:
- Денормализация. Поскольку Elasticsearch не реляционная база, то лучший способ оптимизировать запросы — избегать JOIN-подобных операций, вкладывать нужные данные сразу в документ (хранить вложенные (англ.
nested) объекты). - Вложенные документы (англ.
Nested fields). Если данные часто меняются и денормализация невозможна, то можно использовать типnested. Тогда можно искать и фильтровать вложенные объекты без отдельного запроса.
{
"mappings": {
"properties": {
"posts": {
"type": "nested"
}
}
}
}- Multi-search, batch-запросы. Если предыдущие опции не подходят, батчим запросы и отправляем их вместе
POST /_msearch
{ }
{ "query": { "term": { "user_id": "123" } } }
{ }
{ "query": { "term": { "user_id": "124" } } }
...- Запрос
termsпозволяет передать списком id пользователей. Таким образом вместо n + 1 запросов имеем 1 + 1 запрос (один на пользователей, один на посты):
GET /posts/_search
{
"query": {
"terms": {
"user_id": [ "123", "124", "125", "126" ] // список всех id пользователей
}
}
}Проблема n + 1 запросов в Mongoose часто возникает при использовании populate(), особенно в ситуациях, когда у нас есть список сущностей, и для каждой из них мы подгружаем связанные данные. Это может приводить к выполнению большого количества отдельных запросов к базе, что существенно замедляет приложение.
const UserSchema = new mongoose.Schema({
name: String,
});
const PostSchema = new mongoose.Schema({
title: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
});- Проблема
n + 1:
const posts = await Post.find();
for (const post of posts) {
post.author = await User.findById(post.author);
}- Проблема
n + 1сpopulateв цикле:
const posts = await Post.find();
for (const post of posts) {
await post.populate('author');
}- Многоуровневый
populate:
const posts = await Post.find().populate({
path: 'author',
populate: { path: 'profile' }
});populateна уровне поста (без цикла):
const posts = await Post.find().populate('author').lean();- Использование
$in:
const posts = await Post.find();
const authorIds = posts.map(post => post.author);
const authors = await User.find({ _id: { $in: authorIds } }).lean();
const authorsMap = new Map();
authors.forEach(author => {
authorsMap.set(author._id.toString(), author);
});
const postsWithAuthors = posts.map(post => ({
...post.toObject(),
author: authorsMap.get(post.author.toString()) || null
}));Жадная загрузка (англ. eager loading) — это общий подход для загрузки связанных данных сразу, а populate — конкретный механизм в Mongoose/MongoDB для подстановки связанных данных, который может вести себя как “ленивая” или “жадная” загрузка в зависимости от реализации.
- Eager loading — архитектурный подход загрузки данных сразу (в SQL, Prisma и др.).
const users = await prisma.user.findMany({
include: {
posts: true, // жадная загрузка постов
},
});- Populate — специфическая реализация этого подхода в MongoDB (но часто через дополнительные запросы, если не “optimized populate”).
const users = await User.find().populate('posts');Команда EXPLAIN — основной инструмент анализа производительности запросов.
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';Она позволяет смотреть
- типы доступа к данным (Index Scan, Full table scan и тд)
- используемые индексы и их эффективность
- количество просматриваемых строк
Индексы – важнейший инструмент ускорения поиска запроса Создавайте индекс для полей, которые используются в
JOINWHEREORDER BYGROUP BY
Учитывайте селективность (индексируйте поля с высокой уникальностью).
Пример создания индекса
CREATE INDEX idx_users_email ON users(email);# плохо
SELECT *
# хорошо
SELECT id, username FROM users; # хорошоИспользуйте JOIN вместо подзапросов, если это возможно
# плохо - подзапрос
SELECT id, (SELECT id FROM orders WHERE orders.user_id = users.id) FROM users;
# хорошо - JOIN
SELECT users.id, orders.id FROM users JOIN orders ON orders.user_id = users.id;Задайте LIMIT и OFFSET
SELECT id, username FROM users LIMIT 50 OFFSET 0;OPTIMIZE TABLE your_tableНапример, в MySQL можно сделать так
SET GLOBAL slow_query_log = 'ON';