Updated Postgres SQL tasks.

pull/1/head
Vladimir Protsenko 2 years ago
parent 00b2b24b92
commit fdad2b4411

@ -1,6 +1,6 @@
# Задания
## 1.
## 1. Установка
Установите postgresql на любую виртуальную машину. Перейдите в пользователя `postgres`.
```
$ sudo su - postgres
@ -8,17 +8,17 @@ $ sudo su - postgres
Вы суперпользователь базы данных.
## 2. Основы
В `psql` выполните инициализацю таблиц базы данных
## 2. Знакомство с командной строкой psql
В `psql` выполните инициализацию таблиц базы данных
```
\i scripts/weather.sql
```
Первая команда, которую нужно знать, это как получить помощь по командам:
Первая команда, которую нужно знать, это команда помощи:
```
\?
```
Вы увидите страницу общей помощи по командам терминала. Результаты будут на разбиты на страницы, которые можно пролистать клавишей пробел.
Вы увидите страницу общей помощи по командам терминала. Результаты будут разбиты на страницы, которые можно пролистать клавишей пробел.
Чтобы вывести список возможных SQL команд, наберите
```
@ -35,7 +35,7 @@ $ sudo su - postgres
\l
```
Из командой строки `psql` даже можно выполнить команду linux. Выведите список файлов корневого каталога операционной системы
Из командой строки `psql` также можно выполнить команду linux. Например список файлов корневого каталога операционной системы
```
\! ls -la
```
@ -55,14 +55,14 @@ $ sudo su - postgres
- `\df` - вывод всех функций,
- `\dp` - вывод привилегий доступа для таблиц и представлений.
## 2.2 Пейджер
### 2.2 Пейджер
По умолчанию пейджер `psql`, который разбивает вывод на страницы, включён. Вы можете отключить его установить значение параметра `pager` равным 0.
По умолчанию пейджер `psql`, который разбивает вывод на страницы, включён. Вы можете отключить его установив значение параметра `pager` равным 0.
```
\pset pager 1
\pset pager 0
```
## 2.3 SQL запросы
## 3. Язык SQL
Попробуйте дать базе данных простой запрос
```sql
@ -70,8 +70,6 @@ SELECT event_type FROM weather LIMIT 20;
```
Это запрос на получение данных колонки `event_type` из таблицы `weather`. Количество результатов будет ограничено 20 записями.
### 2.3.1 Буфер многострочного запроса
В терминале вы также можете написать многострочный запрос. Вводите запрос как обычно, используя Enter для переноса строки. `psql` создаст так называемый буфер запроса. Запрос не будет интерпретирован, пока вы не введёте символ `;`. Чтобы сбросить буфер при ошибке ввода, используйте `\r`.
```sql
\r
@ -80,7 +78,7 @@ FROM weather
WHERE state = 'HAWAII';
```
### 2.3.2 Замер времени выполнения запроса
### 3.1 Замер времени выполнения запроса
Вы можете замерить время выполнения запроса, включив его командой `\timing`
```sql
@ -88,7 +86,7 @@ WHERE state = 'HAWAII';
SELECT DISTINCT(event_type) FROM weather LIMIT 40;
```
### 2.4 DDL, DML запросы
### 3.2 Data Definition Language (DDL), Data Modification Language (DML) запросы
SQL запросы, связанные с модификацией данных таблиц называют DML (Data Modification Language) запросами. SQL запросы, связанные с модификацией базы данных называют DDL (Data Definition Language) запросами.
@ -101,43 +99,47 @@ WHERE episode_id = 57676;
Чтобы выйти из консоли введите `\q`.
## 3. Соединения таблиц
## 4. Соединения таблиц
В реляционных базах данные разбиваются на таблицы так, чтобы избыточность хранимой информации была минимальной, запросы и долгосрочная поддержка были эффективными. Это приводит к необходимости объединять информацию из разных таблиц с помощью механизма `JOIN`.
В следующих примерах мы рассмотрим гипотетическую базу данных для управления человеческими ресурсами. Перед выполнением выполните инициализацию базы данных
В этом разделе мы рассмотрим выдуманную базу данных, созданную для HR отдела для управления человеческими ресурсами. Перед выполнением выполните инициализацию базы данных
```sql
\i scripts/hr.sql
```
Схема таблицы Отделы (departments)
```sql
\d departments
```
| Колонка | Связь с |
|-----------------|-----------------------------------------|
| department_id | |
| department_name | |
| location_id | таблицей locations колонкой location_id |
Схема таблицы Отделы (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 |
Схема таблицы Сотрудники (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)
### 4.1 Внутренние соединения (inner join)
Внутреннее соединение является наиболее распространенным и соединяет столбец в одной таблице и со столбцом в другой. В нашей базе данных HR мы хотели бы найти сотрудников и их отделы. Итак, допустим, мы хотим вывести сотрудников и отделы, в которых они числятся:
```sql
SELECT e.employee_id, e.first_name, e.last_name, d.department_name
@ -149,22 +151,24 @@ INNER JOIN departments d ON (e.department_id = d.department_id);
Мы обратились к СУБД на специальном языке и СУБД взяла на себя самую сложную часть, избавив нас от необходимости писать специализированную программу для поиска и выгрузки информации.
### 3.2 Левые внешние соединения (left outer join)
### 4.2 Левые внешние соединения (left outer join)
При использовании механизма `LEFT OUTER JOIN` из левой таблицы вернуться все строки, и если справа не найдётся соответсвия, то в столбцах правой таблицы вместо значений будет присутствовать `NULL`.
В базе данных, с которой мы работаем младшие сотрудники зависят от старших. Ниже приведена схема третьей таблицы `dependents`, которая фиксирует эти отношения.
Схема таблицы Подчиненные (dependents)
```
\d dependents
```
| Колонка | Связь с |
|---------------|---------------------------------------------|
| employee_id | |
| first_name | |
| last_name | |
| employee_id | таблицей employees колонкой employee_id |
Схема таблицы Подчиненные (dependents)
| Колонка | Связь с |
|-------------|---------------------------------------------|
| employee_id | |
| first_name | |
| last_name | |
| employee_id | таблицей `employees` колонкой `employee_id` |
Допустим, что кроме отдела сотрудника нам также нужно вывести его подчинённых:
@ -179,16 +183,18 @@ FROM employees e
Если бы мы использовали `INNER JOIN`, то исключили бы обычных сотрудников из результатов запроса.
### 3.3 Правые внешние соединения (right outer join)
### 4.3 Правые внешние соединения (right outer join)
В ситуации с `RIGHT OUTER JOIN` всё происходит наоборот. При отсутствии соответствующей записи слева для записи таблицы указанной справа значениями `NULL` заполняются столбцы левой таблицы.
Ранее в схеме departments мы видели, что каждый отдел связан с географическим положением через поле `location_id` таблицы Местоположения (locations).
Схема таблицы Местоположения (locations)
```
\d locations
```
Схема таблицы Местоположения (locations)
| Колонка |
|----------------|
| location_id |
@ -206,11 +212,27 @@ FROM departments d
RIGHT OUTER JOIN locations l ON (d.location_id = l.location_id);
```
Результат говорят нам сообщают, что в городе Effingham штата Индиана США нет отдела. Значит офис был закрыт.
Результат нам сообщают, что в городе Effingham штата Индиана США нет отдела. Значит офис был закрыт.
### 4.4 Cоединения на себя (self join)
Соединение на себя - вид соединения, хотя и не имеет специального синтаксиса, стоит упомянуть отдельно. Такой способ соединения полезен, когда таблица содержит иерархические данные. В нашем примере это таблица `employees`.
### 3.4 Cоединения на себя (self join)
```
Table "public.employees"
Column | Type | Collation | Nullable | Default
---------------+------------------------+-----------+----------+----------------------------------
employee_id | integer | | not null | generated by default as identity
first_name | character varying(20) | | |
last_name | character varying(25) | | not null |
start_date | date | | not null | CURRENT_DATE
job_title | character varying(100) | | not null |
salary | numeric(9,2) | | not null |
manager_id | integer | | |
department_id | integer | | |
```
Соединение на себя - это не технический, а концептуальный термин, который стоит упомянуть отдельно. Такой способ соединения полезен, когда таблица содержит иерархические данные. В нашем примере это таблица `employees`. Обратите внимание, что каждому сотруднику назначен менеджер `manager_id`, который в свою очередь сам является сотрудником, а значит иметь своего менеджера уровнем выше.
Обратите внимание, что каждому сотруднику назначен менеджер `manager_id`, который в свою очередь сам является сотрудником, а значит имеет вышестоящего менеджера, если только он не основатель компании.
Используя соединение на себя мы можем запросить у базы данных список сотрудников и их менеджеров
@ -222,7 +244,7 @@ FROM employees e LEFT OUTER JOIN employees m ON (e.manager_id = m.employee_id);
Поздравляем! Вы познакомились с наиболее типичными видами соединений таблиц.
## 4. Транзакции
## 5. Транзакции
Транзакия - это набор действий, которые выполняются атомарно для внешнего наблюдателя. Если часть действий выполнится не может, откатываются все изменения транзакции.
@ -241,7 +263,7 @@ FROM employees e LEFT OUTER JOIN employees m ON (e.manager_id = m.employee_id);
Транзакции позволяют не писать логику возврата в предыдущее состояние, до начала транзакции.
### 4.1 Простейшие транзакции
### 5.1 Простейшие транзакции
Самая простая транзакия состоит из одного запроса на модификацию данны, заключенного между `BEGIN` и `COMMIT`.
```sql
@ -288,7 +310,7 @@ BEGIN;
COMMIT;
```
### 4.2 Комплексные транзакции
### 5.2 Комплексные транзакции
В реальной жизни транзакции состоят из множества действий.
```sql
@ -318,7 +340,7 @@ COMMIT;
Проверьте, что Bob Young не добавился.
### 4.3 Область видимости изменений транзакции
### 5.3 Область видимости изменений транзакции
Что произойдёт, если вы запустили транзакцию, а в этом время к данным, которые вы модифицируете в новой транзакции обратился другой пользователь? Изменения в транзакциях невидимы другим транзакциям, пока транзакция производящие измнения не завершились.
@ -345,7 +367,7 @@ COMMIT;
Такой вид транзакций ещё называют транзакционным язык описания данных (Transactional DDL). Они жизненно важны для миграций баз данных.
### 4.4 Продвинутые транзакции с вложенными точками отката
### 5.4 Продвинутые транзакции с вложенными точками отката
Для некоторый специфичный случаев могут потребоваться вложенные транзакции. Postgres позволяет сделать даже больше - делать точки сохранения внутри транзакции. Точка сохранения (SAVEPOINT) предоставляет указатель на который может ссылаться комадна `ROLLBACK`.
```sql
@ -364,15 +386,213 @@ COMMIT;
Теперь, если вы попробуете найти Bob Young - вы найдёте его, но не найдёте его подчинённых. Что случилось? Эту транзакцию стало возможным выполнить частично из-за использования команды `SAVEPOINT`.
### 4.5 Я в транзакции?
### 5.5 Я в транзакции?
Чтобы узнать идентификатор транзакции, в которой вы находитесь, вызовите команду
```
```sql
SELECT txid_current();
```
Вы можете удивиться, ведь вы не запускали транзакцию. На самом деле Postgres выполняет все команды, даже самые простые в контексте транзакций.
## 6. Индексы
### 6.1 B-Tree
Познакомимся с наиболее распространённым индексом - B-Tree индексом - на примере тестовых данных `weather.sql`.
Попробуем найти всё про снежные бури.
```sql
SELECT *
FROM weather
WHERE event_type = 'Winter Storm';
```
Чтобы проанализировать запрос используйте `EXPLAIN ANALYZE`
```sql
EXPLAIN ANALYZE
SELECT *
FROM weather
WHERE event_type = 'Winter Storm' limit 2;
```
Анализ покажет вам время планирования выполнения запроса `Planning Time` и время выполнения `Execution Time`.
```sql
QUERY PLAN
-------------------------------------------------------------------------------------------------------
Seq Scan on weather (cost=0.00..115.00 rows=11 width=622) (actual time=0.014..0.311 rows=11 loops=1)
Filter: ((event_type)::text = 'Winter Storm'::text)
Rows Removed by Filter: 1189
Planning Time: 0.066 ms
Execution Time: 0.332 ms
(5 rows)
```
Создадим B-Tree индекс для столбца `event_type` таблицы `weather` и повторим анализ запроса.
```sql
CREATE INDEX idx_weather_event_type ON weather(event_type);
```
Видим, что был использован созданный индекс, благодаря чему время выполнения существенно сократилось.
```sql
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on weather (cost=4.36..37.56 rows=11 width=622) (actual time=0.029..0.036 rows=11 loops=1)
Recheck Cond: ((event_type)::text = 'Winter Storm'::text)
Heap Blocks: exact=6
-> 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;`. Выполним другой запрос - найдём снежные бури, которые привели к потере урожая
```sql
EXPLAIN ANALYZE
SELECT *
FROM weather
WHERE event_type = 'Winter Storm'
AND damage_crops > '0';
```
Выдача до создания индекса
```sql
QUERY PLAN
------------------------------------------------------------------------------------------------------
Seq Scan on weather (cost=0.00..118.00 rows=8 width=622) (actual time=0.011..0.399 rows=11 loops=1)
Filter: (((damage_crops)::text > '0'::text) AND ((event_type)::text = 'Winter Storm'::text))
Rows Removed by Filter: 1189
Planning Time: 0.073 ms
Execution Time: 0.419 ms
(5 rows)
```
Создадим индекс
```sql
CREATE INDEX idx_storm_crop ON weather(event_type,damage_crops);
```
Выдача после создания индекса
```sql
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on weather (cost=4.36..29.69 rows=8 width=622) (actual time=0.022..0.029 rows=11 loops=1)
Recheck Cond: (((event_type)::text = 'Winter Storm'::text) AND ((damage_crops)::text > '0'::text))
Heap Blocks: exact=6
-> Bitmap Index Scan on idx_storm_crop (cost=0.00..4.36 rows=8 width=0) (actual time=0.018..0.018 rows=11 loops=1)
Index Cond: (((event_type)::text = 'Winter Storm'::text) AND ((damage_crops)::text > '0'::text))
Planning Time: 0.203 ms
Execution Time: 0.048 ms
(7 rows)
```
Время уменьшилось на порядок.
Вы можете вывести существующие индексы базы данных sql запросом
```sql
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'weather';
```
### 6.3 Анализ производительности
Как мы увидели, добавление индексов может играет важную роль в производительности вашего приложения. Индексы особенно ценны для больших таблиц.
Выполним следующий запрос, чтобы узнать размер таблиц в количестве записей и процентное соотношение обращений, по индексам и без них.
```sql
SELECT
relname,
100 * idx_scan / (seq_scan + idx_scan) percent_of_time_index_used,
n_live_tup rows_in_table
FROM pg_stat_user_tables
WHERE seq_scan + idx_scan > 0
ORDER BY n_live_tup DESC;
```
Идеальном случае мы хотим видеть `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;`). Подумайте о том, чтобы увеличить это значение перед созданием больших индексов.
## 7. Проблема разбухания БД Postgres
У Postgres есть особый способ обработки обновлений или удалений. При удалении данных место на диске не освобождается до тех пор, пока база данных не будет запущена специальный процесс очистки `vacuum`. Если база данных загружена большим количеством DML запросов на удаление строк, через какое-то время в ней появится много неиспользуемого пространства, что негативно повлияет на производительность, если ничего не сделать.
Попробуйте следующий запрос https://wiki.postgresql.org/wiki/Show_database_bloat, чтобы увидеть находится ли база данных в оптимальном или раздутом (bloat) состоянии.
```sql
SELECT
current_database(), schemaname, tablename, /*reltuples::bigint, relpages::bigint, otta,*/
ROUND((CASE WHEN otta=0 THEN 0.0 ELSE sml.relpages::float/otta END)::numeric,1) AS tbloat,
CASE WHEN relpages < otta THEN 0 ELSE bs*(sml.relpages-otta)::BIGINT END AS wastedbytes,
iname, /*ituples::bigint, ipages::bigint, iotta,*/
ROUND((CASE WHEN iotta=0 OR ipages=0 THEN 0.0 ELSE ipages::float/iotta END)::numeric,1) AS ibloat,
CASE WHEN ipages < iotta THEN 0 ELSE bs*(ipages-iotta) END AS wastedibytes
FROM (
SELECT
schemaname, tablename, cc.reltuples, cc.relpages, bs,
CEIL((cc.reltuples*((datahdr+ma-
(CASE WHEN datahdr%ma=0 THEN ma ELSE datahdr%ma END))+nullhdr2+4))/(bs-20::float)) AS otta,
COALESCE(c2.relname,'?') AS iname, COALESCE(c2.reltuples,0) AS ituples, COALESCE(c2.relpages,0) AS ipages,
COALESCE(CEIL((c2.reltuples*(datahdr-12))/(bs-20::float)),0) AS iotta /* very rough approximation, assumes all cols */
FROM (
SELECT
ma,bs,schemaname,tablename,
(datawidth+(hdr+ma-(case when hdr%ma=0 THEN ma ELSE hdr%ma END)))::numeric AS datahdr,
(maxfracsum*(nullhdr+ma-(case when nullhdr%ma=0 THEN ma ELSE nullhdr%ma END))) AS nullhdr2
FROM (
SELECT
schemaname, tablename, hdr, ma, bs,
SUM((1-null_frac)*avg_width) AS datawidth,
MAX(null_frac) AS maxfracsum,
hdr+(
SELECT 1+count(*)/8
FROM pg_stats s2
WHERE null_frac<>0 AND s2.schemaname = s.schemaname AND s2.tablename = s.tablename
) AS nullhdr
FROM pg_stats s, (
SELECT
(SELECT current_setting('block_size')::numeric) AS bs,
CASE WHEN substring(v,12,3) IN ('8.0','8.1','8.2') THEN 27 ELSE 23 END AS hdr,
CASE WHEN v ~ 'mingw32' THEN 8 ELSE 4 END AS ma
FROM (SELECT version() AS v) AS foo
) AS constants
GROUP BY 1,2,3,4,5
) AS foo
) AS rs
JOIN pg_class cc ON cc.relname = rs.tablename
JOIN pg_namespace nn ON cc.relnamespace = nn.oid AND nn.nspname = rs.schemaname AND nn.nspname <> 'information_schema'
LEFT JOIN pg_index i ON indrelid = cc.oid
LEFT JOIN pg_class c2 ON c2.oid = i.indexrelid
) AS sml
ORDER BY wastedbytes DESC;
```
Колонки показывающие объём места, занимаемого удалёнными данными `wastedbytes` и индекс `wastedibytes` покажут наличие проблемы.
Postgres по умолчанию включает регулярное выполнение процедуры очистки. Вы можете посмотреть время последней запросом
```sql
SELECT relname, last_vacuum, last_autovacuum
FROM pg_stat_user_tables;
```
Очистку также можно запустить вручную утилитой `vacuumdb`. Попробуйте это сделать и вызвать запрос ещё раз.
## Релевантные источники
- Мартин Грабер — Понимание SQL

Loading…
Cancel
Save