Начиная с первых курсов университета, первой ассоциацией у студентов ИТ специальностей со словом «ключ» в базах данных является идентификатор. В принципе это верно, однако следует это понятие расширить – связать его с целостностью данных и производительностью. Рассмотрим эволюцию подходов к работе с ключами на примере роста выдуманной компании NeoCat.

Логический уровень: ключи как идентификаторы
Котята часто делают переводы. Поэтому возникла необходимость чтобы кто-то вел их учет. Именно этим и занимается компания NeoCat. Она начинала как скромный стартап. На начальном этапе разработчики рассматривали ключи исключительно как инструменты обеспечения уникальности и непротиворечивости информации. Команда состояла всего из трех разработчиков, которые предпочитали простые и понятные решения, максимально отражающие бизнес-процессы компании. Целевой таблицей стартапа является таблица payments. Бизнес-процесс был изначально устроен так, что по одному клиенту мог проводиться только один платеж в день.
CREATE TABLE payments (
-- Неуникальное поле, у одного клиента может быть несколько платежей
client_name VARCHAR(20) NOT NULL,
-- Номер платежа формируется как {МЕСЯЦ}-{НОМЕР_В_МЕСЯЦЕ}, например, 03-005. Нумерация начинается заново каждый месяц.
payment_number VARCHAR(6) NOT NULL,
-- Дата совершения платежа
payment_date DATE NOT NULL,
-- Сумма платежа
amount DECIMAL(10,2)
);На этом этапе команда NeoCat неосознанно столкнулась с палитрой концепций ключей. Набор атрибутов, который позволяет обеспечить уникальную идентификацию, называется кандидатским (потенциальным) ключом, то есть он является кандидатом на то, чтобы быть первичным ключом. В момент становления одного ключа первичным, все остальные кандидатские ключи становятся альтернативными ключами. Наконец, суперключ - это такой набор атрибутов таблицы, по которому каждая запись однозначно идентифицируется, даже если часть атрибутов в этом наборе не является необходимой.
В нашем случае рассматривая различные варианты идентификации записей, они обнаружили суперключи, это комбинации вроде (client_name, payment_number, payment_date, amount) или даже (client_name, payment_number, payment_date) - все они гарантировали уникальность, но содержали явно избыточные поля.
Кандидатские ключи:
(client_name, payment_date) - наиболее логичная комбинация, отражающая бизнес-процесс
(client_name, payment_number, EXTRACT(YEAR FROM payment_date)) - потенциальный ключ теоретически возможный, но громоздкий составной ключ.
После выбора первичного ключа, оставшийся кандидат стал альтернативным ключом. Котики-разработчики изменили сущность payments, чтобы она отразила выбранный ими подход.
CREATE TABLE payments (
client_name VARCHAR(20) NOT NULL,
payment_number VARCHAR(6) NOT NULL,
payment_date DATE NOT NULL,
amount DECIMAL(10,2),
PRIMARY KEY (client_name, payment_date)
);Выбор естественного составного ключа, который имеет понятный бизнес-смысл казался идеальным решением. Сотрудники сразу понимали, что идентификатор платежа состоит из наименования клиента и даты. Не требовалось создавать искусственные идентификаторы, что упрощало начальную разработку.
Но даже на этом первом этапе выбора первичного ключа есть нюансы: в классической реляционной модели первичный ключ действительно формально объединяет ограничения UNIQUE и NOT NULL, но в реальности современных СУБД такое уравнение работает лишь как удобное упрощение. Ключевая роль PK — быть структурным якорем и декларацией основного принципа организации данных, и эта роль может реализовываться по-разному: в ClickHouse PK определяет лишь порядок сортировки и партиционирования, допуская дубликаты; в Cassandra — распределение данных по узлам; а некоторые SQL-СУБД в определённых условиях тоже допускают NULL в PK. Таким образом, уникальность и запрет NULL — это распространённые, но не универсальные технические средства для реализации более глубокой архитектурной функции ключа как главного идентификатора сущности. Более того, сама необходимость уникальности не всегда очевидна. Можно ли представить таблицу без единого ключа? Технически — да, но такая таблица быстро превратится в хранилище противоречивых и неуправляемых дубликатов. Таким образом, ключ — это не просто «клей», связывающий таблицы, а архитектурный инструмент, который навязывает данным внутреннюю логику и структуру, реализуя реляционную модель.
Очень скоро у NeoCat проявились фундаментальные проблемы. Бизнес-правила изменились: оказалось, что одному клиенту можно переводить несколько платежей в день. Это простое изменение полностью разрушило уникальность выбранного первичного ключа. Простое на первый взгляд решение оказалось хрупким и негибким перед лицом меняющихся бизнес-требований. Этот первый этап стал для NeoCat ценным уроком: естественные ключи, несмотря на свою понятность, могут стать источником проблем при масштабировании бизнеса и изменении правил. Команда осознала, что стабильность и гибкость часто важнее первоначальной простоты.
Столкнувшись с ограничениями естественных ключей, NeoCat начала экспериментировать с различными подходами. Первой попыткой стали вычисляемые ключи, они искусственно сформированы из бизнес-атрибутов, и их значения понятны человеку. Классический пример - конкатенированные ключи, где несколько полей объединяются в одну строку:
CREATE TABLE payments (
client_name VARCHAR(20) NOT NULL,
payment_number VARCHAR(6) NOT NULL,
payment_date DATE NOT NULL,
amount DECIMAL(10,2),
payment_code VARCHAR(100) GENERATED ALWAYS AS
(client_name || '_' || payment_date || '_' || LPAD(payment_number::text, 3, '0')) STORED,
PRIMARY KEY (payment_code)
);Вычисляемый ключ payment_code решает проблему уникальности, но порождает новые сложности — громоздкие индексы и проблемы с производительностью при частых обновлениях базовых полей, от которых зависел вычисляемый ключ. Более того, при построении связей с другими таблицами разработчики столкнулись с необходимостью сложных проверок, что усложняло JOIN-операции и снижало производительность запросов.

Уровень ограничений: ключи как инструменты бизнес-логики
С ростом бизнеса и усложнением процессов NeoCat столкнулась с необходимостью реализации сложных валидаций и специализированных ключей для конкретных сценариев. Компания расширяла свою функциональность, добавляя внешние ключи для обеспечения ссылочной целостности:
CREATE TABLE payments (
payment_number VARCHAR(6) NOT NULL,
payment_date DATE NOT NULL,
amount DECIMAL(10,2),
client_id BIGINT NOT NULL,
payment_code VARCHAR(50) GENERATED ALWAYS AS
(client_id || '_' || payment_date || '_' || LPAD(payment_number::text, 3, '0')) STORED,
PRIMARY KEY (payment_code)
FOREIGN KEY (client_id) REFERENCES clients(client_id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);Внешние ключи — это не инструмент для идентификации записей, они гарантируют целостность данных, автоматически предотвращая появление некорректных ссылок между таблицами. Они позволяют декларативно описывать бизнес‑правила на уровне схемы БД, упрощая логику приложения за счет автоматизации каскадных операций. Это переносит проверки целостности с прикладного уровня на уровень СУБД, повышая надежность системы. Конфигурация вроде ON UPDATE CASCADE обеспечивает автоматическое распространение изменений на всех связанных таблицах, что значительно упрощает операции обновления данных и поддерживает согласованность информации во всей системе. ON DELETE RESTRICT предотвращает удаление записей, на которые существуют ссылки или установки NULL. Как и другие настройки они мощны, но требуют осторожности.
Ключевой недостаток внешних ключей - снижение производительности при массовых операциях из-за дополнительных проверок. В высоконагруженных OLTP-системах без дополнительных настроек они могут создавать каскадные блокировки, приводя к конфликтам и снижению параллелизма. Поддержка внешних ключей усложняет миграции схемы и ограничена во многих распределенных и аналитических БД. Кроме того, они иногда маскируют проблемы дизайна схемы, когда вместо нормализации добавляются избыточные связи.
Также были созданы дедупликационные ключи для контроля качества данных в той самой таблице clients, на которую ссылалась наша исходная таблица:
CREATE TABLE clients (
client_id BIGINT PRIMARY KEY,
email VARCHAR(100),
phone VARCHAR(20),
tax_id VARCHAR(30),
-- дедупликационный ключ
UNIQUE(email, phone, tax_id)
);Дедупликационные ключи — это наборы атрибутов, по которым определяется дубликат записи, они допускают NULL‑значения и служат именно для бизнес‑логики контроля качества. Такой подход позволял находить потенциальные дубликаты клиентов без жестких ограничений на обязательность заполнения всех полей. На практике это означало, что система могла идентифицировать дубликаты даже при частично заполненной информации, что было особенно важно в сценариях постепенного сбора данных о клиентах. Отметим, что в нашем примере UNIQUE ограничение служит примером попытки контролировать дубликаты, но в реальных системах дедупликация часто требует более сложной, вероятностной или процедурной логики.
Физический уровень: суррогатные ключи и производительность
По мере роста объема данных и увеличения нагрузки NeoCat столкнулась с серьезными проблемами производительности, особенно в JOIN-операциях. Необходимость стабильных идентификаторов, не зависящих от изменений бизнес-логики, привела к переходу на суррогатные ключи - искусственные идентификаторы, генерируемые системой исключительно для целей связи таблиц.
Команда NeoCat реализовала генерацию идентификаторов, начиная с максимального номера платежа в системе, используя последовательности.
-- Специальная последовательность с шагом 1, начинающаяся с 100
CREATE SEQUENCE payment_id_seq
START WITH 100
INCREMENT BY 1;
CREATE TABLE payments (
payment_id BIGINT PRIMARY KEY DEFAULT nextval('payment_id_seq'),
payment_number VARCHAR(6) NOT NULL,
payment_date DATE NOT NULL,
amount DECIMAL(10,2),
client_id BIGINT NOT NULL,
FOREIGN KEY (client_id) REFERENCES clients(client_id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);Однако они столкнулись с проблемой: последовательности использовались без кэширования, что оказалось дорогим удовольствием для производительности. Простое добавление кэша увеличило производительность в десятки раз.
CREATE SEQUENCE payment_id_seq
START WITH 100
INCREMENT BY 1
CASHE 100;Есть нюанс: использование кэша повышает производительность, но ценой потери строгой последовательности. При сбое или перезагрузке БД все кэшированные, но не использованные значения теряются, образуя «дырки» в нумерации. Также каждый SEQUENCE требует места в системном каталоге. Следовательно, если требуется гарантированная непрерывность последовательности ключей — что критично для аудита, борьбы с мошенничеством или соблюдения таких норм, как законы о кассовых аппаратах, — кэшированное решение не подходит.
Переход на суррогатные ключи показал значительное улучшение производительности: операции соединения таблиц ускорились в несколько раз благодаря более эффективному сравнению целочисленных значений вместо строковых. Но в связи с вышеперечисленными минусами решено было перейти на классический автоинкремент:
CREATE TABLE payments (
payment_id BIGSERIAL PRIMARY KEY,
payment_number VARCHAR(6) NOT NULL,
payment_date DATE NOT NULL,
amount DECIMAL(10,2),
client_id BIGINT NOT NULL,
FOREIGN KEY (client_id) REFERENCES clients(client_id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);Использование BIGSERIAL предоставило команде NeoCat автоматическое управление последовательностью, что упростило разработку и снизило вероятность ошибок. Использование дискового пространства для индексов сократилось, так как последовательные ID лучше упаковывались в страницы данных. Разработчики осознали, что эксперимент с ручным SEQUENCE не принес дополнительной пользы бизнесу. Бизнес-пользователи не заметили разницы в формате ID, но ощутили улучшение производительности системы.

Распределенный уровень: UUID и глобальная уникальность
Экспоненциальный рост бизнеса NeoCat потребовал перехода к распределенной системе баз данных. Это привело к внедрению UUID (Universally Unique Identifier). Ключевым преимуществом UUID стала независимо и гарантировано уникальная генерация идентификаторов в любой точке системы без централизованной координации, что критически важно для горизонтального масштабирования и отказоустойчивости. Глобальная уникальность упростила слияние данных из разных источников и предоставила гибкость — например, генерацию ID на стороне клиента до сохранения, что улучшило отзывчивость интерфейсов и работу в офлайн-режимах.
CREATE TABLE payments (
-- gen_random_uuid() генерирует UUID версии 4 для PostgreSQL 13+
payment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
payment_number VARCHAR(6) NOT NULL,
payment_date DATE NOT NULL,
amount DECIMAL(10,2),
client_id BIGINT NOT NULL,
FOREIGN KEY (client_id) REFERENCES clients(client_id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);Изначальный выбор UUID v4 был обусловлен его идеальной уникальностью и безопасностью благодаря случайному характеру генерации. Однако на практике команда NeoCat столкнулась с серьезными проблемами производительности. Случайный характер UUID v4 приводил к сильной фрагментации индексов, что значительно снижало производительность операций вставки и чтения. Каждая новая запись с высокой вероятностью попадала на разные страницы индекса, вызывая большое количество операций ввода-вывода. Решением стал переход к UUIDv7 с временной сортировкой:
CREATE TABLE payments (
-- это функция из расширения uuid-ossp
payment_id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
payment_number VARCHAR(6) NOT NULL,
payment_date DATE NOT NULL,
amount DECIMAL(10,2),
client_id BIGINT NOT NULL,
FOREIGN KEY (client_id) REFERENCES clients(client_id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);Переход на UUID v7 стал ключевым архитектурным решением для NeoCat. Временная компонента в UUID v7 обеспечивает естественное упорядочивание записей, что значительно уменьшает фрагментацию данных. Новые записи добавляются последовательно в конец индексов, что снижает количество операций разделения страниц и улучшает общую производительность системы. При этом сохраняется глобальная уникальность идентификаторов, необходимая для распределенной архитектуры. Наши котята отметили, что данный ключ хорошо подходит для независимой загрузки данных из разных источников.
Основными минусами UUID v7 остаются его большой размер (16 байт против 4-8 у целочисленных ключей), что увеличивает объем индексов и снижает эффективность хранения, а также повышенная сложность реализации, требующая поддержки со стороны библиотек и фреймворков. Упорядоченность на основе времени, являясь преимуществом для производительности, одновременно раскрывает информацию о моменте создания объекта и требует синхронизации системных часов между инстансами для сохранения корректной последовательности. Кроме того, его структура менее случайна по сравнению с UUID v4, что может быть недостатком в сценариях, где критична полная непредсказуемость идентификаторов, например, если он используется для контроля доступа
Несмотря на долгую эволюцию и разнообразие форматов — таких как UUID, GUID, ULID, OID или CUID — каждый из которых часто привязан к конкретному технологическому стеку, в современной практике доминирующим и наиболее универсальным подходом к генерации распределённых идентификаторов остаются варианты UUID/GUID, адаптированные под различные сценарии использования.
Data Warehouse уровень: хеш-ключи и идемпотентность
В компании объем данных начал измеряться в петабайтах. Все чаще и чаще идут разговоры об эффективности потоков данных и идемпотентности данных. Кроме того, аналитики хотели бы тоже рост эффективности в выполнении запросов. Разработчики пришли к общему консенсусу - надо переходить на детерминированные хэши. Они хороши тем, что одни и те же входные данные, переданные в одну и ту же хеш-функцию, всегда дают абсолютно одинаковый результат. В нашем случае хэш-ключ будет искусственно вычисляться на основе бизнес-ключа. Это частный случай естественных ключей, атрибут или набор атрибутов, которые уже используются в бизнес-процессах для однозначной идентификации объекта.
Пример на SQL (используем sha256, можно рассматривать MD5)
CREATE TABLE payments (
-- первичный ключ - детерминированный хэш
payment_hash BYTEA PRIMARY KEY DEFAULT sha256(
( payment_number ||
'|' || payment_date::TEXT ||
'|' || client_id::TEXT
)::bytea
),
-- Бизнес-атрибуты
payment_number VARCHAR(6) NOT NULL,
payment_date DATE NOT NULL,
amount DECIMAL(10,2),
client_id BIGINT NOT NULL,
-- Внешний ключ
FOREIGN KEY (client_id) REFERENCES clients(client_id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
-- Бизнес-ограничения
UNIQUE(payment_number, payment_date, client_id)
);За счет того, что JOIN по хешу чаще всего выполняется быстрее, вырастет и производительность сложных запросов. Часто упоминают такой недостаток, как теоретическая вероятность коллизии, но для современных алгоритмов, таких как SHA-256, она стремится к минимуму (например, для 50% шанса её найти потребуется сгенерировать порядка 2^128 хешей, что выходит далеко за пределы возможностей современных вычислительных систем), поэтому на практике этим риском пренебрегают. Однако, если требования к системе предполагают абсолютную, а не вероятностную уникальность, её можно гарантировать, проверяя при вставке наличие уже существующей записи с таким же хешем. Это обеспечит уникальность ценой производительности, так как каждая операция вставки будет сопровождаться дополнительным поиском. Нужно также учитывать необходимость явной обработки NULL-значений, так как стандартное хеширование NULL в разных системах может давать разные результаты, что нарушает детерминизм. Однако архитектор должен знать о самом факте ее существования, особенно в системах с экстремально большим объемом данных или высокими требованиями к безопасности. Требует дополнительного шага в потоках данных для расчета хеша перед вставкой
Enterprise-уровень: гибридная архитектура и комплексное решение
Становление NeoCat как enterprise-компании с комплексными требованиями к производительности, целостности данных, глобальной уникальности и идемпотентности операций потребовало разработки гибридной архитектуры - это проектирование и построение ИТ-систем, которые целенаправленно комбинируют различные архитектурные подходы, парадигмы, технологии или модели развёртывания для достижения оптимального баланса между преимуществами каждой из них.
CREATE TABLE payments (
-- УРОВЕНЬ 1: ПРОИЗВОДИТЕЛЬНОСТЬ
-- Суррогатный ключ для максимальной скорости JOIN внутри БД
payment_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
-- УРОВЕНЬ 2: ГЛОБАЛЬНАЯ ИДЕНТИФИКАЦИЯ
-- UUID для API, интеграций, репликации
payment_uuid UUID DEFAULT uuid_generate_v7() UNIQUE NOT NULL,
-- УРОВЕНЬ 3: БИЗНЕС-ЛОГИКА
-- Естественные ключи для бизнес-правил
payment_number VARCHAR(6) NOT NULL,
payment_date DATE NOT NULL,
client_id BIGINT NOT NULL,
-- УРОВЕНЬ 4: ИДЕМПОТЕНТНОСТЬ И ЦЕЛОСТНОСТЬ
-- Хэш для гарантированной детерминированности
data_hash BYTEA NOT NULL DEFAULT sha256(
( payment_number ||
'|' || payment_date::TEXT ||
'|' || client_id::TEXT
)::bytea
),
-- Данные
amount DECIMAL(10,2),
-- ОГРАНИЧЕНИЯ ДЛЯ КАЖДОГО УРОВНЯ:
-- 1. Бизнес-правила (естественный ключ)
CONSTRAINT uq_business_key UNIQUE (payment_number, payment_date, client_id),
-- 2. Идемпотентность
CONSTRAINT uq_data_hash UNIQUE (data_hash),
-- 3. Референциальная целостность
CONSTRAINT fk_client
FOREIGN KEY (client_id) REFERENCES clients(client_id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
-- 4. Валидация данных
CONSTRAINT chk_amount_positive CHECK (amount >= 0)
);Гибридный подход позволяет NeoCat достигать компромисса между противоречивыми требованиями: высокая внутренняя производительность обеспечивается компактными числовыми ключами, глобальная уникальность - UUID, целостность бизнес-данных реализуется естественными ключами, а надёжность потоков данных - детерминированными хешами. Главный плюс такой архитектуры оптимизация под конкретные сценарии без глобальных компромиссов, что даёт гибкость для эволюции системы. Однако это достигается ценой значительного усложнения модели данных, роста объёма хранения за счёт дублирования ключей и индексов, а также повышенных требований к квалификации разработчиков, которые должны чётко понимать назначение каждого типа идентификатора в разных контекстах, что увеличивает стоимость поддержки и замедляет онбординг новых членов команды. Не создавайте гибридную архитектуру «на вырост». Начинайте с простого суррогатного ключа и усложняйте схему только тогда, когда столкнётесь с реальными ограничениями, а не гипотетическими сценариями.
Котики разработчики рефлексируют

Путь NeoCat демонстрирует, что каждый тип ключа решает определенный класс задач. Зрелость компании проявилась не в отказе от «старых» ключей, а в понимании, где и когда каждый из них раскрывает свои сильные стороны. Выбор типа ключа — это всегда компромисс между производительностью, масштабируемостью, простотой разработки и бизнес-требованиями, и оптимальное решение зависит от конкретного контекста и стадии развития системы.
Дополнительно: Классификация ключей
Классификация ключей в базах данных — это живая и постоянно развивающаяся область. Из-за многообразия ключей их полезно классифицировать по нескольким критериям. Ниже представлены основные оси классификации.
