|
|
# Задания
|
|
|
|
|
|
## 1.
|
|
|
Установите postgresql на любую виртуальную машину. Перейдите в пользователя `postgres`.
|
|
|
```
|
|
|
$ sudo su - postgres
|
|
|
```
|
|
|
|
|
|
Вы суперпользователь базы данных.
|
|
|
|
|
|
## 2. Основы
|
|
|
В `psql` выполните инициализацю таблиц базы данных
|
|
|
```
|
|
|
\i scripts/weather.sql
|
|
|
```
|
|
|
|
|
|
Первая команда, которую нужно знать, это как получить помощь по командам:
|
|
|
```
|
|
|
\?
|
|
|
```
|
|
|
Вы увидите страницу общей помощи по командам терминала. Результаты будут на разбиты на страницы, которые можно пролистать клавишей пробел.
|
|
|
|
|
|
Чтобы вывести список возможных SQL команд, наберите
|
|
|
```
|
|
|
\h
|
|
|
```
|
|
|
|
|
|
Чтобы вывести информацию о команде, добавьте её после `\h`. Например
|
|
|
```
|
|
|
\h create database
|
|
|
```
|
|
|
|
|
|
Выведите список всех баз данных
|
|
|
```
|
|
|
\l
|
|
|
```
|
|
|
|
|
|
Из командой строки `psql` даже можно выполнить команду linux. Выведите список файлов корневого каталога операционной системы
|
|
|
```
|
|
|
\! ls -la
|
|
|
```
|
|
|
|
|
|
### 2.1 Описание таблиц
|
|
|
|
|
|
Команда `\d` без параметров выведет список всех таблиц и объектов в текущей базе данных. Если добавить символ `+`, для каждой таблицы можно увидеть дополнительную информацию такую как количество занимаемого места на диске.
|
|
|
```
|
|
|
\d+ weather
|
|
|
```
|
|
|
|
|
|
Существует множество `\d`-команд. Наиболее часто используемые:
|
|
|
- `\dt` - вывод всех таблиц,
|
|
|
- `\dn` - вывод всех схем,
|
|
|
- `\dv` - вывод всех представлений (views),
|
|
|
- `\du` - вывод всех пользователей,
|
|
|
- `\df` - вывод всех функций,
|
|
|
- `\dp` - вывод привилегий доступа для таблиц и представлений.
|
|
|
|
|
|
## 2.2 Пейджер
|
|
|
|
|
|
По умолчанию пейджер `psql`, который разбивает вывод на страницы, включён. Вы можете отключить его установить значение параметра `pager` равным 0.
|
|
|
```
|
|
|
\pset pager 1
|
|
|
```
|
|
|
|
|
|
## 2.3 SQL запросы
|
|
|
|
|
|
Попробуйте дать базе данных простой запрос
|
|
|
```sql
|
|
|
SELECT event_type FROM weather LIMIT 20;
|
|
|
```
|
|
|
Это запрос на получение данных колонки `event_type` из таблицы `weather`. Количество результатов будет ограничено 20 записями.
|
|
|
|
|
|
### 2.3.1 Буфер многострочного запроса
|
|
|
|
|
|
В терминале вы также можете написать многострочный запрос. Вводите запрос как обычно, используя Enter для переноса строки. `psql` создаст так называемый буфер запроса. Запрос не будет интерпретирован, пока вы не введёте символ `;`. Чтобы сбросить буфер при ошибке ввода, используйте `\r`.
|
|
|
```sql
|
|
|
\r
|
|
|
SELECT DISTINCT(event_type)
|
|
|
FROM weather
|
|
|
WHERE state = 'HAWAII';
|
|
|
```
|
|
|
|
|
|
### 2.3.2 Замер времени выполнения запроса
|
|
|
|
|
|
Вы можете замерить время выполнения запроса, включив его командой `\timing`
|
|
|
```sql
|
|
|
\timing
|
|
|
SELECT DISTINCT(event_type) FROM weather LIMIT 40;
|
|
|
```
|
|
|
|
|
|
### 2.4 DDL, DML запросы
|
|
|
|
|
|
SQL запросы, связанные с модификацией данных таблиц называют DML (Data Modification Language) запросами. SQL запросы, связанные с модификацией базы данных называют DDL (Data Definition Language) запросами.
|
|
|
|
|
|
Попробуйте в командой строке простой DML запрос
|
|
|
```sql
|
|
|
UPDATE weather
|
|
|
SET magnitude = 40
|
|
|
WHERE episode_id = 57676;
|
|
|
```
|
|
|
|
|
|
Чтобы выйти из консоли введите `\q`.
|
|
|
|
|
|
## 3. Соединения таблиц
|
|
|
|
|
|
В реляционных базах данные разбиваются на таблицы так, чтобы избыточность хранимой информации была минимальной, запросы и долгосрочная поддержка были эффективными. Это приводит к необходимости объединять информацию из разных таблиц с помощью механизма `JOIN`.
|
|
|
|
|
|
В следующих примерах мы рассмотрим гипотетическую базу данных для управления человеческими ресурсами. Перед выполнением выполните инициализацию базы данных
|
|
|
```sql
|
|
|
\i scripts/hr.sql
|
|
|
```
|
|
|
|
|
|
Схема таблицы Отделы (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`.
|
|
|
|
|
|
Мы обратились к СУБД на специальном языке и СУБД взяла на себя самую сложную часть, избавив нас от необходимости писать специализированную программу для поиска и выгрузки информации.
|
|
|
|
|
|
### 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
|
|
|
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);
|
|
|
```
|
|
|
|
|
|
Этим запросом мы объединяем таблицу сотрудников слева и подчинённых справа. Если у сотрудника нет подчинённых, его строке будет сопоставлена строка с значениями `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);
|
|
|
```
|
|
|
|
|
|
Результат говорят нам сообщают, что в городе 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
|
|
|
FROM employees e LEFT OUTER JOIN employees m ON (e.manager_id = m.employee_id);
|
|
|
```
|
|
|
|
|
|
Поздравляем! Вы познакомились с наиболее типичными видами соединений таблиц.
|
|
|
|
|
|
|
|
|
## 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 |