From fdad2b4411ae89e96737f711780d09001734bf96 Mon Sep 17 00:00:00 2001 From: Vladimir Protsenko Date: Fri, 28 Oct 2022 01:00:50 +0400 Subject: [PATCH] Updated Postgres SQL tasks. --- module2/05_sql_postgresql/tasks.md | 328 ++++++++++++++++++++++++----- 1 file changed, 274 insertions(+), 54 deletions(-) diff --git a/module2/05_sql_postgresql/tasks.md b/module2/05_sql_postgresql/tasks.md index ae0c1e0..d0d950e 100644 --- a/module2/05_sql_postgresql/tasks.md +++ b/module2/05_sql_postgresql/tasks.md @@ -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