Команда `\d` без параметров выведет список всех таблиц и объектов в текущей базе данных. Если добавить символ `+`, для каждой таблицы можно увидеть дополнительную информацию такую как количество занимаемого места на диске.
```
\d+ weather
```
Существует множество `\d`-команд. Наиболее часто используемые:
Это запрос на получение данных колонки `event_type` из таблицы `weather`. Количество результатов будет ограничено 20 записями.
В терминале вы также можете написать многострочный запрос. Вводите запрос как обычно, используя Enter для переноса строки. `psql` создаст так называемый буфер запроса. Запрос не будет интерпретирован, пока вы не введёте символ `;`. Чтобы сбросить буфер при ошибке ввода, используйте `\r`.
SQL запросы, связанные с модификацией данных таблиц называют DML (Data Modification Language) запросами. SQL запросы, связанные с модификацией базы данных называют DDL (Data Definition Language) запросами.
В реляционных базах данные разбиваются на таблицы так, чтобы избыточность хранимой информации была минимальной, запросы и долгосрочная поддержка были эффективными. Это приводит к необходимости объединять информацию из разных таблиц с помощью механизма `JOIN`.
В этом разделе мы рассмотрим выдуманную базу данных, созданную для HR отдела для управления человеческими ресурсами. Перед выполнением выполните инициализацию базы данных
Каждый сотрудник имеет свой идентификационный номер `employee_id`. В таблице хранится также важная информация как имя, фамилия, должность и зарплата. Принадлежность к отделу выражается в наличии связи с таблицей `departments` через поле `department_id`. Поле `manager_id` ведёт к менеджеру сотрудника.
Внутреннее соединение является наиболее распространенным и соединяет столбец в одной таблице и со столбцом в другой. В нашей базе данных HR мы хотели бы найти сотрудников и их отделы. Итак, допустим, мы хотим вывести сотрудников и отделы, в которых они числятся:
Обратите внимание, как мы обращаемся к `employee_id`, `first_name`, `last_name` из таблицы `employees` и к `department_name` из таблицы `departments`. Мы связыаем эти два множества информации в часи `FROM`. Обратите внимание, что `INNER JOIN` и `ON` зарезервированные слова. В примеры выше мы хотим получить пересечение элементов таблицы `employees` и `departments` по полю `department_id`.
Мы обратились к СУБД на специальном языке и СУБД взяла на себя самую сложную часть, избавив нас от необходимости писать специализированную программу для поиска и выгрузки информации.
При использовании механизма `LEFT OUTER JOIN` из левой таблицы вернуться все строки, и если справа не найдётся соответсвия, то в столбцах правой таблицы вместо значений будет присутствовать `NULL`.
В базе данных, с которой мы работаем младшие сотрудники зависят от старших. Ниже приведена схема третьей таблицы `dependents`, которая фиксирует эти отношения.
Этим запросом мы объединяем таблицу сотрудников слева и подчинённых справа. Если у сотрудника нет подчинённых, его строке будет сопоставлена строка с значениями `NULL`. Мы можем увидеть, чтоб PostgreSQL вернула сотрудника John Smith у которого в подчинении два человека: Spoiled and DoNothing Smith, и сотрудника Howard TheDuck, возможно middle python-программиста, у которого никого в подчинении нет.
Если бы мы использовали `INNER JOIN`, то исключили бы обычных сотрудников из результатов запроса.
В ситуации с`RIGHT OUTER JOIN` всё происходит наоборот. При отсутствии соответствующей записи слева для записи таблицы указанной справа значениями `NULL` заполняются столбцы левой таблицы.
Ранее в схеме departments мы видели, что каждый отдел связан с географическим положением через поле `location_id` таблицы Местоположения (locations).
Результат нам сообщают, что в городе Effingham штата Индиана США нет отдела. Значит офис был закрыт.
### 4.4 Cоединения на себя (self join)
Соединение на себя - вид соединения, хотя и не имеет специального синтаксиса, стоит упомянуть отдельно. Такой способ соединения полезен, когда таблица содержит иерархические данные. В нашем примере это таблица `employees`.
Обратите внимание, что каждому сотруднику назначен менеджер `manager_id`, который в свою очередь сам является сотрудником, а значит имеет вышестоящего менеджера, если только он не основатель компании.
Транзакия - это набор действий, которые выполняются атомарно для внешнего наблюдателя. Если часть действий выполнится не может, откатываются все изменения транзакции.
Использование транзакций СУБД позволяет уменьшить количество кода необходимого для работы с данными. Если бы СУБД не управляли состоянием транзакций, тогда управляющий код пришлось бы реализовывать в каждом приложении, что усложняло бы код и приводило к ошибкам. Например, представьте типичную ситуацию регистрации пользователя в системе. На уровне приложения вам пришлось бы выполнить следующие действия:
1. Создать пользователя
2. Проверить, что он создан. Если не создан, завершить процесс с ошибкой.
3. Создать аккаунт.
4. Проверить, что он создан. Если не создан, удалить пользователя, завершить процесс с ошибкой.
5. Соединить аккаунт и ползьзователя.
6. Проверить, что соединение создано. Если не создано, удалить пользователя и аккаунт, завершить процесс с ошибкой.
С механизмом транзакций количество шагов уменьшается в 2 раза:
1. Создать пользователя
2. Создать аккаунт
3. Соединить аккаунт и ползьзователя
Транзакции позволяют не писать логику возврата в предыдущее состояние, до начала транзакции.
Самая простая транзакия состоит из одного запроса на модификацию данны, заключенного между `BEGIN` и `COMMIT`.
```sql
BEGIN;
INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id)
VALUES ('Cris', 'Winslett', current_date, 1, 'Director of the Highwire', 2, 2)
RETURNING employee_id;
COMMIT;
```
Чтобы подтвердить, что данные были успешно внесены в БД выполните запрос
```sql
SELECT * FROM employees
WHERE
first_name = 'Cris' AND
last_name = 'Winslett';
```
Однако, если вы решите откатить транзакцию после изменений вводом команды `ROLLBACK`, то вы увидите другой результат
```sql
BEGIN;
INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id)
VALUES ('Elizabeth', 'Christensen', current_date, 1, 'Master of the Highwire', 2, 2)
RETURNING employee_id;
ROLLBACK;
```
Добавленная строка имела идентификатор 10, но была удалена процедурой отката транзакции.
```sql
SELECT * FROM employees
WHERE
first_name = 'Elizabeth' AND
last_name = 'Christensen';
```
Однако некоторые изменения всё-таки происходят даже в случае отката. Убедитесь в том, что `employee_id` монотонно возрастает, выполнив транзакцию с откатом ещё несколько раз. Это происходит, чтобы у каждой транзакции гарантированно было уникальное значение, взятое из последовательности идентификаторов.
Команда `ROLLBACK` всегда завершает транзакцию и отбрасывает изменения. `COMMIT` выполняется только тогда, когда в ходе выполнения действий транзакции не было ошибок. Попробуйте выполнить следующий пример с ошибкой, чтобы в этом убедиться
В реальной жизни транзакции состоят из множества действий.
```sql
BEGIN;
INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id)
VALUES ('Chris', 'Winslett', current_date, 1, 'Jr Director of the Highwire', 2, 2);
INSERT INTO dependents (first_name, last_name, employee_id)
VALUES ('oldest', 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Chris' AND last_name = 'Winslett'));
INSERT INTO dependents (first_name, last_name, employee_id)
VALUES ('youngest', 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Chris' AND last_name = 'Winslett'));
COMMIT;
```
Если вы попробуете выполнить эту транзакцию два раза подряд, то второй раз получите ошибку. Это произойдет потому, что вложенный запрос второй раз вернёт 2 результата - для внесённого ранее сотрудника и внесённого только что. Таким образом транзакци не даст создать запись-дубликат.
В следующем запросе попробуем внести `null` значение в обязательное поле `first_name` для второго подчинённого. Транзакция не выполнится из-за нарушеного ограничения.
```sql
BEGIN;
INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id)
VALUES ('Bob', 'Young', current_date, 1, 'Jr Director of the Highwire', 2, 2);
INSERT INTO dependents (first_name, last_name, employee_id)
VALUES ('oldest', 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Bob' AND last_name = 'Young'));
INSERT INTO dependents (first_name, last_name, employee_id)
VALUES (null, 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Bob' AND last_name = 'Young'));
Что произойдёт, если вы запустили транзакцию, а в этом время к данным, которые вы модифицируете в новой транзакции обратился другой пользователь? Изменения в транзакциях невидимы другим транзакциям, пока транзакция производящие измнения не завершились.
В Postgres изменения структуры таблиц также не реализуются (committed) пока транзакция не выполнится успешно. Это уникальное свойство мире баз данных. Проведём два эксперимента - с корректной и некорректной транзакциями.
```sql
BEGIN;
ALTER TABLE employees
ADD COLUMN niddle_name VARCHAR(50) DEFAULT NULL;
COMMIT;
```
Проверьте, что структура таблицы изменилась командой `\d employees`.
Теперь попробуем транзакцию с ошибкой:
```sql
BEGIN;
ALTER TABLE employees ADD COLUMN address_line_1 VARCHAR(50) DEFAULT NULL;
ALTER TABLE employees ADD COLUMN address_line_2 VARCHAR(50) DEFAULT NULL;
ALTER TABLE employees ADD COLUMN city VARCHAR(50) DEFAULT NULL;
ALTER TABLE employees ADD COLUMN province VARCHAR(50) DEFAULT NULL;
ALTER TABLE employees ADD COLUMN postal_code VARCHAR(50) NOT NULL;
COMMIT;
```
Из-за того, что изменения структуры были внутри одной большой транзакции и значение по умолчанию для `postal_code` не было указано никакие изменения не были применены. Проверьте, что структура таблицы не изменилась.
Такой вид транзакций ещё называют транзакционным язык описания данных (Transactional DDL). Они жизненно важны для миграций баз данных.
Для некоторый специфичный случаев могут потребоваться вложенные транзакции. Postgres позволяет сделать даже больше - делать точки сохранения внутри транзакции. Точка сохранения (SAVEPOINT) предоставляет указатель на который может ссылаться комадна `ROLLBACK`.
```sql
BEGIN;
INSERT INTO employees (first_name, last_name, start_date, salary, job_title, manager_id, department_id)
VALUES ('Bob', 'Young', current_date, 1, 'Jr Director of the Highwire', 2, 2);
SAVEPOINT saved_employee;
INSERT INTO dependents (first_name, last_name, employee_id)
VALUES ('oldest', 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Bob' AND last_name = 'Young'));
INSERT INTO dependents (first_name, last_name, employee_id)
VALUES (null, 'kid', (SELECT employee_id FROM employees WHERE first_name = 'Bob' AND last_name = 'Young'));
ROLLBACK TO SAVEPOINT saved_employee;
COMMIT;
```
Теперь, если вы попробуете найти Bob Young - вы найдёте его, но не найдёте его подчинённых. Что случилось? Эту транзакцию стало возможным выполнить частично из-за использования команды `SAVEPOINT`.
-> Bitmap Index Scan on idx_weather_event_type (cost=0.00..4.36 rows=11 width=0) (actual time=0.025..0.025 rows=11 loops=1)
Index Cond: ((event_type)::text = 'Winter Storm'::text)
Planning Time: 0.234 ms
Execution Time: 0.057 ms
(7 rows)
```
### 6.2 Многоколоночный индекс
Postgres позволяет создавать сложные индексы для нескольких колонок. Как и в случае с одной колонкой, если вы выполняете запрос по нескольких колонкам часто, такой индекс позволит выполнять его эффективнее.
Удалите старый индекс командой `DROP INDEX idx_weather_event_type;`. Выполним другой запрос - найдём снежные бури, которые привели к потере урожая
Идеальном случае мы хотим видеть `99%` для таблиц больших чем 10000 записей. Если вы нашли такую таблицу, где это не так - она хороший кандидат для создания новых индексов.
Также полезной может быть информация о том, сколько блоков индексированных данных читается из кэша Postgres, а сколько с диска.
```sql
SELECT
sum(idx_blks_read) as idx_read,
sum(idx_blks_hit) as idx_hit,
(sum(idx_blks_hit) - sum(idx_blks_read)) / sum(idx_blks_hit) as ratio
FROM pg_statio_user_indexes;
```
Упомянем ещё пару хитростей.
Использование команды `CREATE INDEX CONCURRENTLY` позволит избежать блокировки таблицы.
Параметр `maintenance_work_mem` определяет объём памяти задействованный под процесс создания индексов и другие фоновые задачи, например `vacuum`. По умолчанию это значение равно 64 МБ (вы можете найти его из psql в списке `select name, setting from pg_settings;`). Подумайте о том, чтобы увеличить это значение перед созданием больших индексов.
У Postgres есть особый способ обработки обновлений или удалений. При удалении данных место на диске не освобождается до тех пор, пока база данных не будет запущена специальный процесс очистки `vacuum`. Если база данных загружена большим количеством DML запросов на удаление строк, через какое-то время в ней появится много неиспользуемого пространства, что негативно повлияет на производительность, если ничего не сделать.
Попробуйте следующий запрос https://wiki.postgresql.org/wiki/Show_database_bloat, чтобы увидеть находится ли база данных в оптимальном или раздутом (bloat) состоянии.