diff --git a/module2/05_sql_postgresql/tasks.md b/module2/05_sql_postgresql/tasks.md index 30ed68b..ae0c1e0 100644 --- a/module2/05_sql_postgresql/tasks.md +++ b/module2/05_sql_postgresql/tasks.md @@ -1,20 +1,19 @@ # Задания ## 1. -Установите postgresql на любую виртуальную машину. +Установите postgresql на любую виртуальную машину. Перейдите в пользователя `postgres`. +``` +$ sudo su - postgres +``` -## 2. -Пройдите все уроки https://www.crunchydata.com/developers/tutorials по SQL запросам. Далее следует перевод заданий. +Вы суперпользователь базы данных. -При выполнении на локальном компьютере используйте скрипт `weather.sql` для инициализации базы данных. В `psql` выполните +## 2. Основы +В `psql` выполните инициализацю таблиц базы данных ``` \i scripts/weather.sql ``` -## 2.1 Основы - -Вы суперпользователь. - Первая команда, которую нужно знать, это как получить помощь по командам: ``` \? @@ -41,7 +40,7 @@ \! ls -la ``` -### 2.1.1 Описание таблиц +### 2.1 Описание таблиц Команда `\d` без параметров выведет список всех таблиц и объектов в текущей базе данных. Если добавить символ `+`, для каждой таблицы можно увидеть дополнительную информацию такую как количество занимаемого места на диске. ``` @@ -49,20 +48,21 @@ ``` Существует множество `\d`-команд. Наиболее часто используемые: + - `\dt` - вывод всех таблиц, - `\dn` - вывод всех схем, - `\dv` - вывод всех представлений (views), - `\du` - вывод всех пользователей, - `\df` - вывод всех функций, - `\dp` - вывод привилегий доступа для таблиц и представлений. -### 2.1.2 Пейджер +## 2.2 Пейджер По умолчанию пейджер `psql`, который разбивает вывод на страницы, включён. Вы можете отключить его установить значение параметра `pager` равным 0. ``` \pset pager 1 ``` -### 2.1.3 SQL запросы +## 2.3 SQL запросы Попробуйте дать базе данных простой запрос ```sql @@ -70,7 +70,7 @@ SELECT event_type FROM weather LIMIT 20; ``` Это запрос на получение данных колонки `event_type` из таблицы `weather`. Количество результатов будет ограничено 20 записями. -### 2.1.3.1 Буфер многострочного запроса +### 2.3.1 Буфер многострочного запроса В терминале вы также можете написать многострочный запрос. Вводите запрос как обычно, используя Enter для переноса строки. `psql` создаст так называемый буфер запроса. Запрос не будет интерпретирован, пока вы не введёте символ `;`. Чтобы сбросить буфер при ошибке ввода, используйте `\r`. ```sql @@ -80,7 +80,7 @@ FROM weather WHERE state = 'HAWAII'; ``` -### 2.1.3.2 Замер времени выполнения запроса +### 2.3.2 Замер времени выполнения запроса Вы можете замерить время выполнения запроса, включив его командой `\timing` ```sql @@ -88,7 +88,7 @@ WHERE state = 'HAWAII'; SELECT DISTINCT(event_type) FROM weather LIMIT 40; ``` -### 2.1.4 DDL, DML запросы +### 2.4 DDL, DML запросы SQL запросы, связанные с модификацией данных таблиц называют DML (Data Modification Language) запросами. SQL запросы, связанные с модификацией базы данных называют DDL (Data Definition Language) запросами. @@ -101,40 +101,281 @@ WHERE episode_id = 57676; Чтобы выйти из консоли введите `\q`. -## 2.2 Соединения таблиц +## 3. Соединения таблиц В реляционных базах данные разбиваются на таблицы так, чтобы избыточность хранимой информации была минимальной, запросы и долгосрочная поддержка были эффективными. Это приводит к необходимости объединять информацию из разных таблиц с помощью механизма `JOIN`. -Для этих примеров мы рассмотрим гипотетическую базу данных для приложения для человеческих ресурсов.Это приложение отслеживает отделы, сотрудников, их менеджеры и зарплаты. Перед выполнением выполните инициализацию базы данных -``` +В следующих примерах мы рассмотрим гипотетическую базу данных для управления человеческими ресурсами. Перед выполнением выполните инициализацию базы данных +```sql \i scripts/hr.sql ``` -### 2.2.1 Внутренние соединения (inner join) +Схема таблицы Отделы (departments) +```sql +\d departments +``` +| Колонка | Связь с | +|-----------------|-----------------------------------------| +| department_id | | +| department_name | | +| location_id | таблицей locations колонкой location_id | + +Схема таблицы Сотрудники (employees) +``` +\d employees +``` +| Колонка | Связь с | +|---------------|---------------------------------------------| +| employee_id | | +| first_name | | +| last_name | | +| start_date | | +| job_title | | +| salary | | +| manager_id | таблицей employees колонкой employee_id | +| department_id | таблицей departments колонкой department_id | + +Каждый сотрудник имеет свой идентификационный номер `employee_id`. В таблице хранится также важная информация как имя, фамилия, должность и зарплата. Принадлежность к отделу выражается в наличии связи с таблицей `departments` через поле `department_id`. Поле `manager_id` ведёт к менеджеру сотрудника. + +### 3.1 Внутренние соединения (inner join) +Внутреннее соединение является наиболее распространенным и соединяет столбец в одной таблице и со столбцом в другой. В нашей базе данных HR мы хотели бы найти сотрудников и их отделы. Итак, допустим, мы хотим вывести сотрудников и отделы, в которых они числятся: ```sql SELECT e.employee_id, e.first_name, e.last_name, d.department_name FROM employees e INNER JOIN departments d ON (e.department_id = d.department_id); ``` +Обратите внимание, как мы обращаемся к `employee_id`, `first_name`, `last_name` из таблицы `employees` и к `department_name` из таблицы `departments`. Мы связыаем эти два множества информации в часи `FROM`. Обратите внимание, что `INNER JOIN` и `ON` зарезервированные слова. В примеры выше мы хотим получить пересечение элементов таблицы `employees` и `departments` по полю `department_id`. + +Мы обратились к СУБД на специальном языке и СУБД взяла на себя самую сложную часть, избавив нас от необходимости писать специализированную программу для поиска и выгрузки информации. -### 2.2.2 Левые внешние соединения (left outer join) +### 3.2 Левые внешние соединения (left outer join) + +При использовании механизма `LEFT OUTER JOIN` из левой таблицы вернуться все строки, и если справа не найдётся соответсвия, то в столбцах правой таблицы вместо значений будет присутствовать `NULL`. + +В базе данных, с которой мы работаем младшие сотрудники зависят от старших. Ниже приведена схема третьей таблицы `dependents`, которая фиксирует эти отношения. + +Схема таблицы Подчиненные (dependents) +``` +\d dependents +``` +| Колонка | Связь с | +|---------------|---------------------------------------------| +| employee_id | | +| first_name | | +| last_name | | +| employee_id | таблицей employees колонкой employee_id | + + +Допустим, что кроме отдела сотрудника нам также нужно вывести его подчинённых: ```sql -SELECT e.employee_id, e.first_name, e.last_name, d.department_name, dep.first_name || ' ' || dep.last_name AS dependent +SELECT e.employee_id, e.first_name, e.last_name, d.department_name, dep.first_name || ' ' || dep.last_name AS dependent FROM employees e INNER JOIN departments d ON (e.department_id = d.department_id) LEFT OUTER JOIN dependents dep ON (e.employee_id = dep.employee_id); ``` - -### 2.2.3 Правые внешние соединения (right outer join) + +Этим запросом мы объединяем таблицу сотрудников слева и подчинённых справа. Если у сотрудника нет подчинённых, его строке будет сопоставлена строка с значениями `NULL`. Мы можем увидеть, чтоб PostgreSQL вернула сотрудника John Smith у которого в подчинении два человека: Spoiled and DoNothing Smith, и сотрудника Howard TheDuck, возможно middle python-программиста, у которого никого в подчинении нет. + +Если бы мы использовали `INNER JOIN`, то исключили бы обычных сотрудников из результатов запроса. + +### 3.3 Правые внешние соединения (right outer join) + +В ситуации с `RIGHT OUTER JOIN` всё происходит наоборот. При отсутствии соответствующей записи слева для записи таблицы указанной справа значениями `NULL` заполняются столбцы левой таблицы. + +Ранее в схеме departments мы видели, что каждый отдел связан с географическим положением через поле `location_id` таблицы Местоположения (locations). + +Схема таблицы Местоположения (locations) +``` +\d locations +``` +| Колонка | +|----------------| +| location_id | +| street_address | +| postal_code | +| city | +| state_province | +| country_id | + +Узнаем, где географически расположены отделы компании + ```sql SELECT d.department_name, l.city, l.state_province, l.country_id FROM departments d RIGHT OUTER JOIN locations l ON (d.location_id = l.location_id); ``` -### 2.2.4 Cоединения на себя (self join) +Результат говорят нам сообщают, что в городе Effingham штата Индиана США нет отдела. Значит офис был закрыт. + +### 3.4 Cоединения на себя (self join) + +Соединение на себя - это не технический, а концептуальный термин, который стоит упомянуть отдельно. Такой способ соединения полезен, когда таблица содержит иерархические данные. В нашем примере это таблица `employees`. Обратите внимание, что каждому сотруднику назначен менеджер `manager_id`, который в свою очередь сам является сотрудником, а значит иметь своего менеджера уровнем выше. + +Используя соединение на себя мы можем запросить у базы данных список сотрудников и их менеджеров + ```sql -SELECT e.first_name, e.last_name, e.job_title, format('%s %s', m.first_name, m.last_name) AS manager +SELECT e.first_name, e.last_name, e.job_title, format('%s %s', m.first_name, m.last_name) AS manager FROM employees e LEFT OUTER JOIN employees m ON (e.manager_id = m.employee_id); -``` \ No newline at end of file +``` + +Поздравляем! Вы познакомились с наиболее типичными видами соединений таблиц. + + +## 4. Транзакции + +Транзакия - это набор действий, которые выполняются атомарно для внешнего наблюдателя. Если часть действий выполнится не может, откатываются все изменения транзакции. + +Использование транзакций СУБД позволяет уменьшить количество кода необходимого для работы с данными. Если бы СУБД не управляли состоянием транзакций, тогда управляющий код пришлось бы реализовывать в каждом приложении, что усложняло бы код и приводило к ошибкам. Например, представьте типичную ситуацию регистрации пользователя в системе. На уровне приложения вам пришлось бы выполнить следующие действия: +1. Создать пользователя +2. Проверить, что он создан. Если не создан, завершить процесс с ошибкой. +3. Создать аккаунт. +4. Проверить, что он создан. Если не создан, удалить пользователя, завершить процесс с ошибкой. +5. Соединить аккаунт и ползьзователя. +6. Проверить, что соединение создано. Если не создано, удалить пользователя и аккаунт, завершить процесс с ошибкой. + +С механизмом транзакций количество шагов уменьшается в 2 раза: +1. Создать пользователя +2. Создать аккаунт +3. Соединить аккаунт и ползьзователя + +Транзакции позволяют не писать логику возврата в предыдущее состояние, до начала транзакции. + +### 4.1 Простейшие транзакции + +Самая простая транзакия состоит из одного запроса на модификацию данны, заключенного между `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) + VALUES ('Tome', 'Jones') + RETURNING employee_id; +COMMIT; +``` + +### 4.2 Комплексные транзакции + +В реальной жизни транзакции состоят из множества действий. +```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')); +COMMIT; +``` + +Проверьте, что Bob Young не добавился. + +### 4.3 Область видимости изменений транзакции + +Что произойдёт, если вы запустили транзакцию, а в этом время к данным, которые вы модифицируете в новой транзакции обратился другой пользователь? Изменения в транзакциях невидимы другим транзакциям, пока транзакция производящие измнения не завершились. + +В 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). Они жизненно важны для миграций баз данных. + +### 4.4 Продвинутые транзакции с вложенными точками отката + +Для некоторый специфичный случаев могут потребоваться вложенные транзакции. 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`. + + +### 4.5 Я в транзакции? + +Чтобы узнать идентификатор транзакции, в которой вы находитесь, вызовите команду +``` +SELECT txid_current(); +``` + +Вы можете удивиться, ведь вы не запускали транзакцию. На самом деле Postgres выполняет все команды, даже самые простые в контексте транзакций. + + +## Релевантные источники +- Мартин Грабер — Понимание SQL +- https://www.postgresql.org/docs/ +- https://theartofpostgresql.com +- https://www.crunchydata.com/developers/tutorials \ No newline at end of file