Что такое первичный ключ (Primary Key)?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Первичный ключ (Primary Key): определение, свойства и практика
Первичный ключ (Primary Key) — это столбец или комбинация столбцов в таблице, которые однозначно идентифицируют каждую строку. Это фундаментальное понятие в реляционных БД.
Определение и свойства
Основные свойства Primary Key
1. Уникальность (Uniqueness) Жчение PK не может повторяться в таблице. Каждая строка имеет свой PK.
CREATE TABLE users (
id INT PRIMARY KEY, -- id=1 может быть только один раз
email VARCHAR(100),
name VARCHAR(100)
);
-- Попытка вставить дубликат → ERROR
INSERT INTO users VALUES (1, 'john@example.com', 'John');
INSERT INTO users VALUES (1, 'jane@example.com', 'Jane'); -- ERROR!
2. Не NULL (NOT NULL) Значение PK не может быть NULL. Иначе строка не будет однозначно идентифицироваться.
INSERT INTO users VALUES (NULL, 'john@example.com', 'John'); -- ERROR!
3. Неизменяемость (Immutability) В идеале PK не должен меняться, так как на него ссылаются другие таблицы через Foreign Keys.
-- Плохо (нарушает referential integrity)
UPDATE users SET id = 2 WHERE id = 1;
-- Потому что orders.user_id может ссылаться на id=1
4. Минимальность (Minimality) PK должен быть минимальным — не включать "лишние" столбцы.
❌ Плохо (включает лишний столбец):
PRIMARY KEY (user_id, email, created_at)
✅ Хорошо (только уникальный идентификатор):
PRIMARY KEY (user_id)
Типы Primary Keys
1. Простой PK (Single column)
CREATE TABLE products (
id INT PRIMARY KEY, -- INT, BIGINT, или UUID
name VARCHAR(100),
price DECIMAL(10, 2)
);
Типы значений:
-
AUTO_INCREMENT (MySQL) / SERIAL (PostgreSQL)
CREATE TABLE orders ( id SERIAL PRIMARY KEY, -- 1, 2, 3, ... order_date DATE, total DECIMAL(10, 2) ); -
UUID (Universally Unique Identifier)
CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(100), name VARCHAR(100) ); -- id = "507f1f77-bcf8-6cd7-9943-9011"
Сравнение INT vs UUID:
| Параметр | INT/BIGINT | UUID |
|---|---|---|
| Размер | 4-8 байт | 16 байт |
| 读读 | Быстро | Медленнее (индексы больше) |
| Коллизии | SERIAL = уникально | Очень низкая вероятность |
| Распределённые системы | Сложно | ✅ Идеально |
| Прямой доступ | ✅ id=42 | Нужен UUID |
| Security | Угадать следующий ID | ❌ Нельзя угадать |
2. Составной PK (Composite Primary Key)
Когда одного столбца недостаточно для уникальности, используется комбинация.
CREATE TABLE student_enrollments (
student_id INT NOT NULL,
course_id INT NOT NULL,
enrollment_date DATE,
grade CHAR(1),
PRIMARY KEY (student_id, course_id) -- Комбинация уникальна
);
-- Можно один и тот же student в разных courses
INSERT INTO student_enrollments VALUES (1, 101, '2025-01-15', 'A');
INSERT INTO student_enrollments VALUES (1, 102, '2025-01-15', 'B'); -- ✅ OK
INSERT INTO student_enrollments VALUES (1, 101, '2025-02-01', 'A'); -- ❌ ERROR! (дубликат)
Другой пример:
CREATE TABLE user_addresses (
user_id INT NOT NULL,
address_type VARCHAR(20) NOT NULL, -- 'home', 'work', 'billing'
street VARCHAR(255),
city VARCHAR(100),
PRIMARY KEY (user_id, address_type)
);
-- User может иметь несколько адресов, но не несколько адресов одного типа
INSERT INTO user_addresses VALUES (1, 'home', '123 Main St', 'NY');
INSERT INTO user_addresses VALUES (1, 'work', '456 Office Blvd', 'NY'); -- ✅ OK
INSERT INTO user_addresses VALUES (1, 'home', '789 Other St', 'NY'); -- ❌ ERROR!
Primary Key vs Unique Key
| Параметр | Primary Key | Unique Key |
|---|---|---|
| Количество | Только 1 | Много |
| NULL допустим | ❌ Нет | ✅ Да (может быть много NULL) |
| Индекс | ✅ Автоматически | ✅ Автоматически |
| Foreign Key | ✅ Может быть ссылка | ❌ Обычно нет |
| Пример | id | email (уникален, но может быть NULL в legacy) |
CREATE TABLE users (
id INT PRIMARY KEY, -- Один PK
email VARCHAR(100) UNIQUE, -- Unique key (но может быть NULL)
username VARCHAR(100) UNIQUE, -- Ещё один unique key
phone VARCHAR(20) -- Обычный столбец
);
INSERT INTO users VALUES (1, 'john@example.com', 'john', '555-1234');
INSERT INTO users VALUES (2, NULL, 'jane', '555-5678'); -- ✅ OK (NULL допустим в UNIQUE)
INSERT INTO users VALUES (3, NULL, 'bob', '555-9999'); -- ✅ OK (несколько NULL в UNIQUE)
INSERT INTO users VALUES (4, 'john@example.com', 'john2', '555-0000'); -- ❌ ERROR (дубликат email)
Primary Key и Foreign Key
Primary Key в одной таблице ссылается на другие таблицы через Foreign Key:
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT NOT NULL,
amount DECIMAL(10, 2),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- user_id в orders ссылается на id (PK) в users
Каскадное удаление (ON DELETE CASCADE):
-- Если удалить пользователя, все его заказы удалятся тоже
DELETE FROM users WHERE id = 1;
-- → Все строки с user_id=1 в orders удалятся автоматически
Выбор Primary Key на практике
Сценарий 1: Таблица с простым идентификатором
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY, -- AUTO_INCREMENT
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL, -- URL-friendly
content TEXT,
author_id INT NOT NULL REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Запрос:
SELECT * FROM articles WHERE id = 42; -- Быстро (индекс на PK)
Сценарий 2: Распределённая система (микросервисы)
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type VARCHAR(50),
user_id UUID, -- Может быть из другой системы
data JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
Преимущества UUID:
- Генерируется на клиенте (не нужно ждать DB)
- Работает в распределённых системах
- Нельзя угадать следующий ID (security)
Сценарий 3: Слабо связанная таблица (junction table)
CREATE TABLE student_courses (
student_id INT NOT NULL,
course_id INT NOT NULL,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE
);
-- Составной PK является и составным FK
Частые ошибки
❌ Ошибка 1: Нет Primary Key
CREATE TABLE logs (
message VARCHAR(255),
level VARCHAR(10),
created_at TIMESTAMP
-- Нет PK! Может быть несколько идентичных строк
);
✅ Исправление:
CREATE TABLE logs (
id BIGSERIAL PRIMARY KEY, -- Добавить PK
message VARCHAR(255),
level VARCHAR(10),
created_at TIMESTAMP
);
❌ Ошибка 2: Изменяемый Primary Key
-- Плохо: использовать email как PK
CREATE TABLE users (
email VARCHAR(100) PRIMARY KEY, -- Может измениться
name VARCHAR(100)
);
-- Если user изменит email:
-- 1. Нарушается referential integrity (orders.user_email)
-- 2. Сложный UPDATE CASCADE
✅ Исправление:
CREATE TABLE users (
id INT PRIMARY KEY, -- Стабильный ID
email VARCHAR(100) UNIQUE, -- Уникален, но может измениться
name VARCHAR(100)
);
❌ Ошибка 3: Слишком большой составной PK
CREATE TABLE transactions (
user_id INT NOT NULL,
product_id INT NOT NULL,
timestamp TIMESTAMP NOT NULL,
amount DECIMAL(10, 2),
PRIMARY KEY (user_id, product_id, timestamp) -- Слишком большой
);
✅ Исправление:
CREATE TABLE transactions (
id BIGSERIAL PRIMARY KEY, -- Простой PK
user_id INT NOT NULL,
product_id INT NOT NULL,
timestamp TIMESTAMP NOT NULL,
amount DECIMAL(10, 2),
INDEX (user_id, product_id) -- Для быстрого поиска
);
Primary Key и производительность
Индексирование: Primary Key автоматически создаёт индекс (B-tree). Это ускоряет:
SELECT WHERE id = XJOIN ON table.id = other.table.idDELETE FROM table WHERE id = X
-- Быстро (PK индекс используется)
SELECT * FROM users WHERE id = 42;
-- Медленно (full table scan)
SELECT * FROM users WHERE email = 'john@example.com';
-- РЕШЕНИЕ: CREATE INDEX idx_users_email ON users(email);
Заключение
Первичный ключ — это:
- Обязателен в каждой таблице
- Уникален и не NULL по определению
- Стабилен (не должен меняться)
- Минимален (ровно столько столбцов, сколько нужно)
- Источник истины для идентификации строк
Выбор между INT vs UUID зависит от архитектуры (монолит → INT, микросервисы → UUID). Главное — выбрать и придерживаться стратегии.