Зачем нужен PRIMARY KEY и FOREIGN KEY (ключи)?
Вот пример
CREATE TABLE instructor (
ID CHAR(5),
name VARCHAR(20) NOT NULL,
dept_name VARCHAR(20),
PRIMARY KEY (ID),
FOREIGN KEY (dept_name) REFERENCES department(dept_name)
);
Разумеется, я читал основы SQL, но они не объясняют зачем нужны эти ключи в работе БД. Первичный ключ ID содержит уникальное значение по которому можно однозначно идентифицировать запись.
Тут первичный ключ ID. Внешний ключ dept_name. Зачем мы пишем перед полем PRIMARY KEY т.е. указываем что это поле первичный ключ? Почему нельзя сделать это поле просто автоинкрементируемым? И будет уникальное поле.
Аналогично - зачем мы пишем перед полем dept_name FOREIGN KEY? То есть мы говорим, что поле dept_name указывает на поле dept_name в другой таблице department. Что это дает?
Я могу не указывать FOREIGN KEY (dept_name) REFERENCES department(dept_name) при создании таблицы. Просто запомнить, что например JOIN по этим полям.
Что делают эти команды? Зачем они нужны?
Ответы (5 шт):
Либо плохо читали, либо читали что-то не то. По пунктам.
PRIMARY KEY. Как выше уже сказали, identity-поле вовсе не гарантирует уникальность значения. Пример ниже - для MS SQL. Создаем таблицу, и добавляем в неё 1 строку:
use tempdb
go
create table dbo.pk_test (
id int identity not null,
name varchar(1)
)
go
insert into dbo.pk_test(name) values('A');
select id, name from dbo.pk_test;
go
id name
----------- ----
1 A
(1 rows affected)
и вставляем ещё одну с таким же id:
begin tran;
set xact_abort on;
set identity_insert dbo.pk_test on;
insert into dbo.pk_test(id, name) values(1, 'B');
set identity_insert dbo.pk_test off;
select id, name from dbo.pk_test;
go
id name
----------- ----
1 A
1 B
(2 rows affected)
– никаких ошибок. Откатываем вставку, вешаем на поле id PRIMARY KEY:
rollback
go
alter table dbo.pk_test add constraint pk_pk_test primary key(id);
go
select id, name from dbo.pk_test;
go
id name
----------- ----
1 A
(1 rows affected)
и снова пытаемся вставить дубль id:
begin tran;
set xact_abort on;
set identity_insert dbo.pk_test on;
insert into dbo.pk_test(id, name) values(1, 'B');
go
Violation of PRIMARY KEY constraint 'pk_pk_test'. Cannot insert duplicate key in object 'dbo.pk_test'.
– получаем ошибку. А в некоторых БД identity-поля отсутствуют вообще - например, в оракле до версии 12c. Вместо них используются генераторы последовательностей (sequence), и для вставки неуникального значения в поле не нужно никаких ухищрений типа set identity_insert. И ещё нюанс PRIMARY/UNIQUE constraints: по сути, это логические ограничения, ограничения бизнес-модели. На физическом уровне эти ограничения всегда реализуются уникальными индексами по соответствующим полям.
FOREIGN KEY: создаем и заполняем тестовые таблицы:
use tempdb
go
create table dbo.fk_source (
id int not null primary key
)
go
create table dbo.fk_target (
fk_id int not null,
constraint fk_target_source foreign key(fk_id) references dbo.fk_source(id)
on update cascade on delete no action
)
go
insert into dbo.fk_source(id) values(1);
insert into dbo.fk_target(fk_id) values(1);
go
select id from dbo.fk_source;
go
id
-----------
1
(1 rows affected)
select fk_id from dbo.fk_target;
go
fk_id
-----------
1
(1 rows affected)
теперь в таблице, на которую ссылается FK, меняем значение поля с FK:
update dbo.fk_source set id=2 where id=1;
go
(1 rows affected)
select fk_id from dbo.fk_target
go
fk_id
-----------
2
(1 rows affected)
– из-за включенной опции каскадного обновления в связанной таблице значение поля обновилось автоматически. Пытаемся удалить запись из таблицы-источника:
delete dbo.fk_source where id=2;
go
Msg 547, Level 16, State 1, Server ., Line 1
The DELETE statement conflicted with the REFERENCE constraint "fk_target_source".
The conflict occurred in database "tempdb", table "dbo.fk_target", column 'fk_id'.
The statement has been terminated.
– FK не позволяет этого сделать. Пытаемся в таблицу-приёмник вставить не существующее в таблице-источнике значение:
insert into dbo.fk_target(fk_id) values(3);
go
Msg 547, Level 16, State 1, Server ., Line 1
The INSERT statement conflicted with the FOREIGN KEY constraint "fk_target_source".
The conflict occurred in database "tempdb", table "dbo.fk_source", column 'id'.
The statement has been terminated.
– FK не позволяет этого сделать. Пытаемся очистить всю таблицу-источник, и вообще удалить её:
truncate table dbo.fk_source;
go
Msg 4712, Level 16, State 1, Server ., Line 1
Cannot truncate table 'dbo.fk_source' because it is being referenced by a FOREIGN KEY constraint.
drop table dbo.fk_source;
go
Msg 3726, Level 16, State 1, Server ., Line 1
Could not drop object 'dbo.fk_source' because it is referenced by a FOREIGN KEY constraint.
– FK не позволяет этого сделать. А теперь удаляем FK:
alter table dbo.fk_target drop constraint fk_target_source
go
– и становится можно всё:
insert into dbo.fk_target(fk_id) values(3);
go
(1 rows affected)
delete dbo.fk_source where id=2;
go
(1 rows affected)
truncate table dbo.fk_source;
go
drop table dbo.fk_source;
go
У обеих конструкций две основные задачи:
- Поддержание целостности
- Самодокументирование кода
PRIMARY KEY гарантирует, что в столбце все значения будут уникальными, вне зависимости от того, как вы их генерируете.
FOREIGN KEY гарантирует, что значение из таблицы точно есть в той таблице, на которую идёт ссылка.
Помимо вышеуказанных причин приведу ещё одну: PRIMARY KEY и FOREIGN KEY зачастую индексируются. Это приводит к тому, что обращение по ним будет происходить быстрее.
Пример на MySql:
Создадим таблицу и заполним её большим числом данных:
CREATE TABLE `table_test_1` (
`field_1` INT NOT NULL AUTO_INCREMENT,
`field_2` INT NOT NULL DEFAULT 0,
`field_3` VARCHAR(255) NOT NULL,
PRIMARY KEY (`field_1`)
);
INSERT INTO `table_test_1` (`field_3`) VALUES ('a1'),('a2'),('a3');
INSERT INTO `table_test_1` (`field_3`) VALUES ('b1'),('b2'),('b3');
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
INSERT INTO `table_test_1` (`field_3`) SELECT `field_3` FROM `table_test_1`;
UPDATE `table_test_1` SET `field_2` = `field_1`;
Теперь у нас есть таблица с первичным ключом field_1 и аналогичным ему значением field_2. Теперь сравним выборки по первичному ключу:
SELECT * FROM `table_test_1` WHERE `field_1` = 555555;
+---------+---------+---------+
| field_1 | field_2 | field_3 |
+---------+---------+---------+
| 555555 | 555555 | b2 |
+---------+---------+---------+
1 row in set (0.00 sec)
И по значению:
SELECT * FROM `table_test_1` WHERE `field_2` = 555555;
+---------+---------+---------+
| field_1 | field_2 | field_3 |
+---------+---------+---------+
| 555555 | 555555 | b2 |
+---------+---------+---------+
1 row in set (0.36 sec)
Разница заметна.
Теперь создадим вторую таблицу:
CREATE TABLE `table_test_2` (
`field_1` INT NOT NULL AUTO_INCREMENT,
`field_2` INT NOT NULL DEFAULT 0,
`field_3` INT NULL DEFAULT NULL,
PRIMARY KEY (`field_1`),
CONSTRAINT `table_test_2_fk0` FOREIGN KEY (`field_3`) REFERENCES `table_test_1` (`field_1`) ON UPDATE RESTRICT ON DELETE RESTRICT
);
INSERT INTO `table_test_2` (`field_1`, `field_2`, `field_3`) SELECT `field_1`, `field_1` FROM `table_test_1`;
Здесь у нас есть внешний ключ field_3 и обычное поле field_2, значения которых содержит значения field_1 (и соответственно field_2) из первой таблицы.
Попробуем INNER JOIN запрос на основе внешнего ключа:
SELECT * FROM `table_test_2` INNER JOIN `table_test_1` ON `table_test_2`.`field_3` = `table_test_1`.`field_1` LIMIT 1 OFFSET 55555;
+---------+---------+---------+---------+---------+---------+
| field_1 | field_2 | field_3 | field_1 | field_2 | field_3 |
+---------+---------+---------+---------+---------+---------+
| 71925 | 71925 | 71925 | 71925 | 71925 | a2 |
+---------+---------+---------+---------+---------+---------+
1 row in set (0.28 sec)
А вот запрос на основе обычных значений:
SELECT * FROM `table_test_2` INNER JOIN `table_test_1` ON `table_test_2`.`field_2` = `table_test_1`.`field_2` LIMIT 1 OFFSET 55555;
+---------+---------+---------+---------+---------+---------+
| field_1 | field_2 | field_3 | field_1 | field_2 | field_3 |
+---------+---------+---------+---------+---------+---------+
| 71925 | 71925 | 71925 | 71925 | 71925 | a2 |
+---------+---------+---------+---------+---------+---------+
1 row in set (0.51 sec)
Возможно эти примеры не самые показательные, но разница заметна уже на них, а для сложных структур данных и для более сложных запросов разница между индексируемыми значениями и неиндексируемыми становится критически важной.
Рассматривать надо с обратной стороны.
Простой индекс.
Дополнительная структура, хранящая указанное выражение для каждой записи, причём в сортированном порядке. Ускоряет выполнение многих операций, во-первых, за счёт более компактного по сравнению с самой таблицей размера (т.е. требует меньше чтений), во-вторых, за счёт сортированности по выражению индекса.
Уникальный индекс.
Отличается от обычного дополнительным условием уникальности выражения для каждой записи. В дополнение к функциям простого индекса обеспечивает быстрый контроль уникальности и не позволяет появляться дубликатам в данных. Однако при наличии NULL в любом из полей выражения не считает такие записи дубликатами (это не относится, например, к OracleDB, которая не различает NULL и пустую строку).
Первичный индекс.
Отличается дополнительным условием запрета значения NULL в полях, использованных в выражении индекса. За счёт этого обеспечивает максимально строгий контроль отсутствия дубликатов, а потому может использоваться (и используется) для уникальной идентификации записи. В некоторых СУБД (например, MySQL/MariaDB) первичный индекс, кроме того, безусловно является кластерным.
А внешний ключ (FOREIGN KEY) - это вообще не структура. Это правило. Правило контроля. Сервер при любом изменении данных проверяет, чтобы оно не было нарушено.
Кстати, для того, чтобы быстро проверять - должен существовать индекс.
Первичный ключ (PRIMARY KEY) - это одному или нескольким столбцам таблицы, которые используются для уникальной идентификации каждой строки таблицы. Он обычно создается с целью обеспечения целостности данных и упрощения связей с другими таблицами. Вы можете сделать поле автоинкрементируемым, но это не является рекомендуемым способом создания первичного ключа. Это может привести к проблемам с целостностью данных, если в какой-то момент вы захотите изменить структуру таблицы или импортировать данные из другой таблицы.
Внешний ключ (FOREIGN KEY) - это столбец или столбцы, которые ссылаются на первичный ключ другой таблицы. Они часто используются для установления связи между таблицами
В таблице instructor внешний ключ dept_name ссылается на первичный ключ dept_name в таблице department. Это означает, что значение dept_name в таблице instructor должно существовать в таблице department. Это помогает обеспечить целостность данных, так как предотвращает внесение несуществующих значений в таблицу instructor.
Определение внешнего ключа также позволяет создавать связи между таблицами при использовании оператора JOIN. Например, можно использовать JOIN, чтобы получить все инструкторы, преподающие в каком-то отделении, и информацию об этом отделении.
В общем, использование первичных и внешних ключей помогает создавать целостные и связанные таблицы, что упрощает работу с ними и обеспечивает целостность данных.
Ниже приведены несколько примеров использования первичных и внешних ключей.
Пример 1
Предположим, у нас есть таблицы customers и orders. Таблица orders содержит информацию о заказах клиентов, а таблица customers содержит информацию о клиентах. Мы хотим, чтобы каждый заказ был связан с определенным клиентом, поэтому в таблице orders мы можем создать внешний ключ customer_id, который ссылается на первичный ключ id в таблице customers. Таким образом, мы можем убедиться, что в таблице orders не будут указаны несуществующие клиенты.
Пример 2
Предположим, у нас есть таблицы products и orders. Таблица products содержит информацию о товарах, а таблица orders содержит информацию о заказах. Мы хотим, чтобы каждый заказ содержал информацию о конкретном товаре.
Вернёмся к instructor с использованием первичного ключа и внешнего ключа:
CREATE TABLE instructor (
ID CHAR(5) PRIMARY KEY,
name VARCHAR(20) NOT NULL,
dept_name VARCHAR(20),
FOREIGN KEY (dept_name) REFERENCES department(dept_name)
);
В этой таблице ID является первичным ключом, то есть уникальным идентификатором для каждой строки таблицы. Поле dept_name является внешним ключом, которое ссылается на поле dept_name в таблице department. Это означает, что значение dept_name в таблице instructor должно существовать в таблице department.
Некоторые примеры SQL-запросов, использующих таблицу instructor
Вставка новой строки в таблицу instructor:
INSERT INTO instructor (ID, name, dept_name) VALUES ('12345', 'John Smith', 'Computer Science');
Обновление информации об инструкторе:
UPDATE instructor SET name = 'Jane Smith' WHERE ID = '12345';
Получение списка всех инструкторов в отделении Computer Science:
SELECT * FROM instructor WHERE dept_name = 'Computer Science';
Получение списка всех инструкторов с информацией об их отделениях:
SELECT instructor.name, department.dept_name
FROM instructor
JOIN department ON instructor.dept_name = department.dept_name;
Получение списка инструкторов, которые преподают в отделении Computer Science, отсортированных по имени:
SELECT * FROM instructor
WHERE dept_name = 'Computer Science'
ORDER BY name ASC;
Удаление всех инструкторов из отделения Mathematics:
DELETE FROM instructor
WHERE dept_name = 'Mathematics';
Эти примеры дают некоторое представление о том, как можно использовать первичные и внешние ключи в таблице instructor. Однако, это далеко не все возможности, которые предлагают первичные и внешние ключи, и вы можете использовать их в различных ситуациях для управления данными в вашей базе данных.
Несколько примеров SQL-запросов, которые, скорее всего, не будут выполнены из-за нарушения целостности данных
Вставка строки в таблицу instructor с несуществующим значением внешнего ключа dept_name:
INSERT INTO instructor (ID, name, dept_name) VALUES ('12345', 'John Smith', 'Physics');
Если в таблице department нет отделения с именем 'Physics', то этот запрос не будет выполнен, так как внешний ключ dept_name в таблице instructor должен ссылаться на существующее отделение в таблице department.
Обновление строки в таблице instructor с несуществующим значением первичного ключа ID:
UPDATE instructor SET name = 'Jane Smith' WHERE ID = '54321';
Если в таблице instructor нет строки с ID '54321', то этот запрос не будет выполнен, так как первичный ключ ID должен существовать в таблице.
Удаление строки из таблицы instructor с несуществующим значением первичного ключа:
DELETE FROM instructor
WHERE ID = '54321';
Если в таблице instructor нет строки с ID '54321', то этот запрос не будет выполнен, так как первичный ключ ID должен существовать в таблице.
Эти примеры показывают, что первичные и внешние ключи используются для обеспечения целостности данных в базе данных. Они запрещают внесение некорректных данных в таблицы и служат механизмом защиты от ошибок.