Создание таблицы из видео: ```sql drop table if exists rroom_user, email cascade; create table rroom_user( user_id bigint generated always as identity primary key, birth_date DATE ); create table email ( email varchar(320), user_id bigint references rroom_user(user_id), is_verified boolean not null default false, primary key (email, user_id) ); ``` Обратите внимание, как здесь создан первичный ключ — он составной, из двух колонок `email` и `user_id`. Так можно делать. В данном случае это оправдано, потому что мы хотим, чтобы была уникальная связка `email` и `user_id`, чтобы эта связка не дублировалась. В такой схеме у нас один мейл может быть у разных пользователей, но при этом только у одного он может быть верифицирован: ```sql create unique index unique_verified_email_per_user on email (email) where is_verified = true; ``` Об индексах мы поговори позже, но сейчас можно проверить, что оно работает. Создадим пару пользователей: ```sql insert into rroom_user(birth_date) values ('1988-07-29') returning user_id; -- 1 insert into rroom_user(birth_date) values ('1996-01-01') returning user_id; -- 2 ``` Обратите внимание на конструкцию `returning`. С её помощью можно одним запросом как вставить данные, так и вернуть какие-то данные этой вставленной записи, в данном случае поле `user_id` генерируется последовательностью, поэтому очень удобно его значение вернуть в одном запросе с `insert`. Теперь создадим два одинаковых мейла у каждого: ```sql insert into email (email, user_id) values ('sterx@rl6.ru', 1); insert into email (email, user_id) values ('sterx@rl6.ru', 2); select * from email; ``` Видим, что у обоих записей `is_verified=false`. Это окей. У нас может быть мейл, принадлежащий сначала одному пользователю, а потом другому — какой-то рабочий мейл, например. То есть в нашей схеме нет жесткой связки один email один аккаунт. Это особенно актуально для телефонов, когда один и тот же номер телефона может менять разных владельцев и в разные периоды времени принадлежать разным пользователям, что совершенно нормально. Для мейлов возможно это в меньшей степени актуально, но пусть будет. Так вот, попробуем верифицировать email сразу у обоих пользователей: ```sql update email set is_verified=true where email='sterx@rl6.ru'; -- ошибка select * from email; update email set is_verified=true where email='sterx@rl6.ru' and user_id=1; select * from email; update email set is_verified=true where email='sterx@rl6.ru' and user_id=2; -- ошибка select * from email; ``` Видим, что может быть только один пользователь с верифицированным мейлом! Это благодаря нашему индексу. Повторюсь, об индексах подробнее поговорим дальше. Кстати, выше я упоминал, но подчеркну ещё раз подход с колонкой `is_deleted`. Зачастую мы не удаляем данные их БД, а просто помечаем их удалёнными, проставляя в колонке `is_deleted` значение `TRUE`. И тогда при каждой выборке активных записей надо будет смотреть, что поле `is_deleted` имеет значение `FALSE`. Тогда у нас все исторические данные будут доступны в таблице и мы сможем при необходимости их использовать в будущем. ```sql create temp table some_entity( entity_id bigint generated always as identity primary key, value text not null, is_deleted boolean not null default false ); insert into some_entity (value) values ('some value'); insert into some_entity (value) values ('some another value'); select * from some_entity where is_deleted=false; update some_entity set is_deleted=true where entity_id=2; select * from some_entity where is_deleted=false; select * from some_entity; ``` Другим подходом может быть перенос удалённых значений в отдельную таблицу, например, так: ```sql drop table if exists some_entity; drop table if exists some_entity_deleted; create temp table some_entity( entity_id bigint generated always as identity primary key, value text not null ); create table some_entity_deleted (like some_entity including defaults including constraints including indexes); insert into some_entity (value) values ('some value'); insert into some_entity (value) values ('some another value'); select * from some_entity; begin; insert into some_entity_deleted select * from some_entity where entity_id=2; delete from some_entity where entity_id=2; commit; ``` Здесь мы создаём таблицу `some_entity` такую же как и раньше, но без поля `is_deleted`, так как в этом случае вся таблица будет хранить в себе только актуальные записи. Затем мы создаём таблицу `some_entity_deleted`, которая копирует структуру, индексы, ограничения и значения колонок по умолчанию такие же как в `some_entity`. И затем мы вставляем в `some_entity` две строки, одну из которых я затем удалю. Операция удаления должна быть атомарной — то есть мы в 1 операции должны вставить запись в таблицу удаленных записей и удалить запись из основной таблицы. Иначе если операция будет неатомарной, то вставка записи может пройти, а удаление не пройти, например. Поэтому мы оборачиваем это в транзакцию — о транзакциях мы поговорим позже. Ну и обращаю ваше внимание, что `insert into таблица select` работает. То есть можно вставлять записи в таблицу из какой-то выборки. Аналогично можно сделать и без явного определения транзакции с помощью CTE, Common Table Expression, который мы изучим дальше: ```sql insert into some_entity (value) values ('some another value'); with deleted_rows as ( delete from some_entity where entity_id=3 returning * ) insert into some_entity_deleted select * from deleted_rows; select * from some_entity; select * from some_entity_deleted; ``` Преимуществом такого подхода без колонки `is_deleted` может быть то, что не надо помнить, что в каждый запрос надо вставлять это условие where `is_deleted=false`. Таблицы для хранения исторических данных можно при желании вынести в отдельную схему, чтобы они не были видны в основной рабочей схеме. Отдельно здесь хочу отметить 2 момента, которые здесь промелькнули в этом уроке. Во-первых, как создать таблицу такую же, как уже имеющаяся таблица: ```sql create table some_entity_deleted (like some_entity including defaults including constraints including indexes); ``` Можно также создать таблицу на основе результатов запроса вместе с результатами этого запроса: ```sql create table some_tmp_table as select * from book; drop table some_tmp_table; ``` Здесь структура создаваемой новой таблицы, то есть типы данных и их размеры, определяется автоматически на основе структуры и типов данных столбцов, которые возвращаются `SELECT`-запросом. И можно вставить в существующую таблицу результаты какого-то запроса: ```sql insert into some_entity_deleted select * from some_entity where entity_id=2; ``` Не то чтобы вы будете этим часто пользоваться, но это довольно удобный механизм в процессе изучения баз данных — дублировать таблицу, вставить туда данные из какого-то запроса и так далее.