# Задания ## 1. Установка Установите postgresql на любую виртуальную машину. Перейдите в пользователя `postgres`. ``` $ sudo su - postgres ``` Вы суперпользователь базы данных. ## 2. Знакомство с командной строкой psql В `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 0 ``` ## 3. Язык SQL Попробуйте дать базе данных простой запрос ```sql SELECT event_type FROM weather LIMIT 20; ``` Это запрос на получение данных колонки `event_type` из таблицы `weather`. Количество результатов будет ограничено 20 записями. В терминале вы также можете написать многострочный запрос. Вводите запрос как обычно, используя Enter для переноса строки. `psql` создаст так называемый буфер запроса. Запрос не будет интерпретирован, пока вы не введёте символ `;`. Чтобы сбросить буфер при ошибке ввода, используйте `\r`. ```sql \r SELECT DISTINCT(event_type) FROM weather WHERE state = 'HAWAII'; ``` ### 3.1 Замер времени выполнения запроса Вы можете замерить время выполнения запроса, включив его командой `\timing` ```sql \timing SELECT DISTINCT(event_type) FROM weather LIMIT 40; ``` ### 3.2 Data Definition Language (DDL), Data Modification Language (DML) запросы SQL запросы, связанные с модификацией данных таблиц называют DML (Data Modification Language) запросами. SQL запросы, связанные с модификацией базы данных называют DDL (Data Definition Language) запросами. Попробуйте в командой строке простой DML запрос ```sql UPDATE weather SET magnitude = 40 WHERE episode_id = 57676; ``` Чтобы выйти из консоли введите `\q`. ## 4. Соединения таблиц В реляционных базах данные разбиваются на таблицы так, чтобы избыточность хранимой информации была минимальной, запросы и долгосрочная поддержка были эффективными. Это приводит к необходимости объединять информацию из разных таблиц с помощью механизма `JOIN`. В этом разделе мы рассмотрим выдуманную базу данных, созданную для HR отдела для управления человеческими ресурсами. Перед выполнением выполните инициализацию базы данных ```sql \i scripts/hr.sql ``` ```sql \d departments ``` Схема таблицы Отделы (departments) | Колонка | Связь с | |-----------------|---------------------------------------------| | department_id | | | department_name | | | location_id | таблицей `locations` колонкой `location_id` | ``` \d employees ``` Схема таблицы Сотрудники (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` ведёт к менеджеру сотрудника. ### 4.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`. Мы обратились к СУБД на специальном языке и СУБД взяла на себя самую сложную часть, избавив нас от необходимости писать специализированную программу для поиска и выгрузки информации. ### 4.2 Левые внешние соединения (left outer join) При использовании механизма `LEFT OUTER JOIN` из левой таблицы вернуться все строки, и если справа не найдётся соответствия, то в столбцах правой таблицы вместо значений будет присутствовать `NULL`. В базе данных, с которой мы работаем младшие сотрудники зависят от старших. Ниже приведена схема третьей таблицы `dependents`, которая фиксирует эти отношения. ``` \d dependents ``` Схема таблицы Подчиненные (dependents) | Колонка | Связь с | |-------------|---------------------------------------------| | dependent_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`, то исключили бы обычных сотрудников из результатов запроса. ### 4.3 Правые внешние соединения (right outer join) В ситуации с `RIGHT OUTER JOIN` всё происходит наоборот. При отсутствии соответствующей записи слева для записи таблицы указанной справа значениями `NULL` заполняются столбцы левой таблицы. Ранее в схеме departments мы видели, что каждый отдел связан с географическим положением через поле `location_id` таблицы Местоположения (locations). ``` \d locations ``` Схема таблицы Местоположения (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 штата Индиана США нет отдела. Значит офис был закрыт. ### 4.4 Соединения на себя (self join) Соединение на себя - вид соединения, хотя и не имеет специального синтаксиса, стоит упомянуть отдельно. Такой способ соединения полезен, когда таблица содержит иерархические данные. В нашем примере это таблица `employees`. ``` 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 | | | ``` Обратите внимание, что каждому сотруднику назначен менеджер `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); ``` Поздравляем! Вы познакомились с наиболее типичными видами соединений таблиц. ## 5. Транзакции Транзакция - это набор действий, которые выполняются атомарно для внешнего наблюдателя. Если часть действий выполнится не может, откатываются все изменения транзакции. Использование транзакций СУБД позволяет уменьшить количество кода необходимого для работы с данными. Если бы СУБД не управляли состоянием транзакций, тогда управляющий код пришлось бы реализовывать в каждом приложении, что усложняло бы код и приводило к ошибкам. Например, представьте типичную ситуацию регистрации пользователя в системе. На уровне приложения вам пришлось бы выполнить следующие действия: 1. Создать пользователя 2. Проверить, что он создан. Если не создан, завершить процесс с ошибкой. 3. Создать аккаунт. 4. Проверить, что он создан. Если не создан, удалить пользователя, завершить процесс с ошибкой. 5. Соединить аккаунт и пользователя. 6. Проверить, что соединение создано. Если не создано, удалить пользователя и аккаунт, завершить процесс с ошибкой. С механизмом транзакций количество шагов уменьшается в 2 раза: 1. Создать пользователя 2. Создать аккаунт 3. Соединить аккаунт и пользователя Транзакции позволяют не писать логику возврата в предыдущее состояние, до начала транзакции. ### 5.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; ``` ### 5.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 не добавился. ### 5.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). Они жизненно важны для миграций баз данных. ### 5.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`. ### 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 - https://www.postgresql.org/docs/ - https://theartofpostgresql.com - https://www.crunchydata.com/developers/tutorials - https://www.sql-ex.ru/ - https://sqlbolt.com/