Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
387 changes: 387 additions & 0 deletions lab_14.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,387 @@
-- Лабораторная работа 14: Анализ и оптимизация высоконагруженных запросов
--
-- Цель: Выполнить анализ сложного SQL-запроса с использованием EXPLAIN ANALYZE и оптимизировать его производительность
--
-- Задача 1: Анализ производительности запроса

explain (analyze, buffers)
select
u.id,
u.email,
u.first_name,
u.last_name,
count(distinct o.id) as order_count,
sum(oi.price * oi.quantity) as total_spent,
max(o.created_at) as last_order_date,
string_agg(distinct c.name, ', ') as favorite_categories
from users u
join orders o on u.id = o.user_id
join order_items oi on o.id = oi.order_id
join products p on oi.product_id = p.id
join categories c on p.category_id = c.id
where o.created_at >= now() - interval '1 year'
and o.status = 'completed'
group by u.id, u.email, u.first_name, u.last_name
order by total_spent desc
limit 10;

-- 1. Какая операция самая дорогая? Посмотрите на "Actual time" для каждой операции.
-- Найдите операцию с наибольшим actual time.

-- самая дорогая - groupaggregate (агрегация по u.id)

-- 2. Есть ли полные сканирования таблиц?

-- есть
-- parallel seq scan on order_items oi
-- parallel seq scan on orders o
-- parallel seq scan on products p
-- seq scan on categories c

-- 3. Сколько строк обрабатывается на каждом этапе?

-- после основных соединений: merge join ... actual rows=2057944
-- после агрегации по пользователю: groupaggregate ... actual rows=79862
-- после сортировки и limit: rows=10


-- 4. Сколько памяти используется для операций сортировки?
-- Есть ли сортировка на диске?

-- да, сортировка на диске есть:
-- sort method: external merge disk: 35928kb (и у воркеров тоже disk: 37288kb и 36120kb)
-- финальная сортировка под order by ... limit 10 лёгкая:
-- sort method: top-n heapsort memory: 38kb (без диска).

-- 5. Как выполняются JOIN? Определите тип каждого JOIN (Nested Loop, Hash Join, Merge Join). Какие JOIN самые дорогие? Почему?

-- parallel hash join (например по oi.order_id = o.id, oi.product_id = p.id)
-- hash join (по p.category_id = c.id)
-- merge join (по o.user_id = u.id)
-- самый дорогой по времени — merge join (actual time=17500.941..20352.878),
-- потому что перед ним нужна сортировка по ключам (o.user_id, o.id), причём
-- она уходит на диск, и объём промежуточных данных очень большой (миллионы строк).

-- 6. Какой процент попаданий в кэш?

-- по верхнему узлу buffers: shared hit=4121 read=49568, доля попаданий примерно
-- 4121 / (4121 + 49568) ≈ 7.7% (остальное — чтение с диска).


-- Задание 1.2: Определите узкие места
-- На основе плана выполнения:
-- 1. Какая операция использует больше всего CPU?

-- groupaggregate и join-цепочка (hash join / parallel hash join), т.к. они обрабатывают миллионы строк.
-- parallel seq scan по orders и order_items, а также parallel hash join (workers planned/launched = 2)

-- 2. Есть ли проблемы с памятью?

-- да. в плане есть сортировка на диске: sort method: external merge disk: 35928kb
-- (и у воркеров тоже disk). также есть temp read/written, значит часть данных
-- уходила во временные файлы. у hash-части видно batches: 8 (hash разбивался на
-- батчи, обычно это признак что в память целиком не влезло).

-- 3. Какие JOIN неэффективны?

--самые тяжёлые — join’ы, которые дают огромный промежуточный набор: merge join ... actual
-- rows≈2057944 (дорогой, потому что перед ним нужна сортировка по ключам и она ушла на диск) +
-- parallel hash join между большими таблицами (orders/order_items/products), т.к. чтение идёт через
-- parallel seq scan (индексы для join-колонок в плане не используются; видно только index scan по users_pkey).

-- Задание 1.3: Анализ существующих индексов
-- 1. Напишите запрос для получения информации о текущих индексах.

select
schemaname,
tablename,
indexname,
indexdef
from pg_indexes
where schemaname = 'public'
order by tablename, indexname;


-- a. Какие индексы созданы для таблиц

-- public.orders: orders_pkey, idx_orders_status_created
-- public.order_items: order_items_pkey, idx_order_items_order_id
-- public.products: products_pkey, idx_products_category_id
-- public.categories: categories_pkey, categories_name_key
-- public.users: users_pkey, users_email_key

-- b. Какие столбцы проиндексированы
-- orders_pkey → (id)
-- idx_orders_status_created → (status, created_at)
-- order_items_pkey → (id)
-- idx_order_items_order_id → (order_id)
-- products_pkey → (id)
-- idx_products_category_id → (category_id)
-- categories_pkey → (id)
-- categories_name_key → (name)
-- users_pkey → (id)
-- users_email_key → (email)


select
relname as table_name,
seq_scan,
seq_tup_read,
idx_scan,
n_live_tup
from pg_stat_user_tables
order by n_live_tup desc;


-- 2. Проверьте статистику по таблицам (pg_stat_user_tables):
-- a. Сколько раз выполнялось полное сканирование

-- order_items: 7398
-- orders: 1105
-- products: 607
-- categories: 586
-- users: 218

-- b. Сколько строк было прочитано

-- order_items: 41,597,246,660
-- orders: 1,063,430,692
-- products: 110,199,601
-- users: 17,200,522
-- categories: 85,814

-- c. Сколько раз использовались индексы

-- order_items: 256
-- orders: 7730
-- products: 7667
-- categories: 680
-- users: 1,395,599

-- d. Сколько "живых" строк

-- order_items: 6,002,640
-- orders: 1,500,000
-- products: 400,000
-- users: 80,000
-- categories: 150

-- Задание 1.4: Проверка влияния параметров сервера

-- 1. Используя запрос к pg_stat_database выведите информацию о количестве временных
-- файлов и объеме данных во временных файлах

select
datname,
temp_files,
pg_size_pretty(temp_bytes) as temp_size
from pg_stat_database
where datname = current_database();

create extension if not exists pg_stat_statements;

-- файлов: 9312 размер: 68 GB

-- 2. Используя запрос к pg_stat_statements выведите информацию о 5 самых "тяжелых"
-- запросах по CPU времени:
select
left(query, 100) as query_snippet,
calls,
round(total_exec_time::numeric, 2) as total_time_ms,
round(mean_exec_time::numeric, 2) as avg_time_ms,
round(
(100 * total_exec_time / nullif(sum(total_exec_time) over (), 0))::numeric,
2
) as percentage,
rows,
round((rows::numeric / nullif(calls, 0))::numeric, 2) as avg_rows
from pg_stat_statements
where query not like '%pg_stat_statements%'
order by total_exec_time desc
limit 5;

-- 3. Проанализируйте и выполните следующий запрос. Объясните результат.

select
pid,
usename,
state,
extract(epoch from (now() - query_start)) as duration_sec,
wait_event_type,
wait_event,
backend_type,
query
from pg_stat_activity
where backend_type = 'client backend'
order by duration_sec desc;



-- запрос показывает активные сессии и тип ожидания (cpu/io/lock) по полям state и wait_event_type.


-- Задача 2: Оптимизация

-- задание 2.1. анализ недостающих индексов
--
-- 1. используя запрос к pg_stat_user_tables найдите таблицы с большим количеством seq scan.
-- результирующая выборка должна предоставлять: имя таблицы, seq_scan, seq_tup_read, idx_scan, n_live_tup,
-- процент использования индексов (index_usage_percent).

select
relname,
seq_scan,
seq_tup_read,
idx_scan,
n_live_tup,
case
when seq_scan + idx_scan = 0 then 0
else round(100.0 * idx_scan / (seq_scan + idx_scan), 2)
end as index_usage_percent
from pg_stat_user_tables
order by seq_scan desc;

-- ответ:
-- самое проблемное место — order_items: seq_scan=7398, idx_scan=256, index_usage_percent=3.34 (почти всё читается seq scan).
-- также заметно categories: seq_scan=586, idx_scan=680, index_usage_percent=53.71 (примерно половина чтений без индекса).
-- orders/products/users по проценту индексов нормальные (orders ~87.49%, products ~92.66%, users ~99.98%).
-- партиции orders_* с idx_scan=0 дают 0%, но они маленькие/редко читаются (по ним отдельные запросы почти не ходят).

--
-- 2. проанализируйте запрос и определите, какие столбцы используются для:
-- a. фильтрации where
-- b. определения условий соединения
-- c. группировки
-- d. сортировки
--
-- ответ:
-- a) where: o.created_at, o.status
-- b) join: u.id = o.user_id; o.id = oi.order_id; oi.product_id = p.id; p.category_id = c.id
-- c) group by: u.id, u.email, u.first_name, u.last_name
-- d) order by: total_spent (sum(oi.price * oi.quantity)) desc

--
-- 3. предложите (предоставьте код, но не создавайте) 3-4 индекса, которые могут ускорить запрос.
-- объясните, почему каждый индекс нужен.
--
-- ответ:
-- 1) ускоряет where по status/created_at и сразу даёт user_id для join/group by
-- create index idx_orders_status_created_user on orders (status, created_at, user_id);

-- 2) отдельно под соединение users <- orders (иногда плану проще так, чем сортировать/мерджить большие объёмы)
-- create index idx_orders_user_id on orders (user_id);

-- 3) под основной join orders -> order_items и расчёт суммы (покрывает нужные поля для sum)
-- create index idx_order_items_order_id_inc on order_items (order_id) include (product_id, price, quantity);

-- 4) под join order_items -> products (для ветки с категориями)
-- create index idx_order_items_product_id on order_items (product_id);

-- Задание 2.2: Настройка параметров соединения
select name, setting, unit
from pg_settings
where name in ('work_mem', 'shared_buffers', 'effective_cache_size');
-- effective_cache_size 524288 8kB
-- shared_buffers 16384 8kB
-- work_mem 4096 1kB
set work_mem = '32MB';

explain (analyze, buffers)
select
u.id,
u.email,
u.first_name,
u.last_name,
count(distinct o.id) as order_count,
sum(oi.price * oi.quantity) as total_spent,
max(o.created_at) as last_order_date,
string_agg(distinct c.name, ', ') as favorite_categories
from users u
join orders o on u.id = o.user_id
join order_items oi on o.id = oi.order_id
join products p on oi.product_id = p.id
join categories c on p.category_id = c.id
where o.created_at >= now() - interval '1 year'
and o.status = 'completed'
group by u.id, u.email, u.first_name, u.last_name
order by total_spent desc
limit 10;

-- ответ: после увеличения work_mem запрос explain (analyze, buffers) не выполнился,
-- ошибка: "error: could not resize shared memory segment ... no space left on device".
-- значит, системе/контейнеру не хватило места под shared memory/временные файлы, сравнить план до/после не удалось.



-- Задача 3: Партиционирование

-- Задание 3.1: Анализ распределения данных

select
date_trunc('month', created_at) as month,
count(*) as cnt
from orders
group by 1
order by 1;

select
min(user_id) as min_user_id,
max(user_id) as max_user_id,
count(distinct user_id) as users_cnt
from orders;

select
status,
count(*) as cnt
from orders
group by status
order by cnt desc;
-- 3.1.a по дате создания (created_at)
-- ответ: распределение по месяцам почти ровное (примерно 57k–65k заказов на месяц).
-- крайние месяцы меньше: 2023-12 = 37,633 и 2025-12 = 24,168 (похоже на неполные месяцы/обрезанный период).
-- вывод: поле created_at хорошо подходит для range-партиционирования, т.к. по времени данные нарезаются равномерно.

-- 3.1.b по идентификатору пользователя (user_id)
-- ответ: min_user_id = 1, max_user_id = 80,000, users_cnt = 80,000.
-- то есть заказы встречаются у всех пользователей, диапазон user_id полный.
-- вывод: партиционирование по user_id имеет смысл только если запросы часто фильтруют по user_id/диапазонам user_id.

-- 3.1.c по статусу (status)
-- ответ: completed = 1,049,894 (~69.99%)
-- pending = 382,910 (~25.53%)
-- shipped = 63,890 (~4.26%)
-- cancelled = 3,306 (~0.22%)
-- вывод: сильный перекос в completed, поэтому партиционирование по status даст слабое отсечение данных для запросов по completed
-- (всё равно будет читаться ~70% таблицы).

-- 3.1.2 по какому полю лучше партиционировать
-- ответ: лучше created_at (range по дате), потому что в вашем “тяжелом” запросе основной фильтр:
-- where o.created_at >= now() - interval '1 year'
-- и по created_at сработает partition pruning (будут читаться только нужные партиции).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


-- Задание 3.2: Создание партиционированной таблицы

create table if not exists orders_partitioned (like orders including defaults including constraints)
partition by range (created_at);

create table if not exists orders_partitioned_2021 partition of orders_partitioned
for values from (timestamp '2021-01-01') to (timestamp '2022-01-01');

create table if not exists orders_partitioned_2022 partition of orders_partitioned
for values from (timestamp '2022-01-01') to (timestamp '2023-01-01');

create table if not exists orders_partitioned_2023 partition of orders_partitioned
for values from (timestamp '2023-01-01') to (timestamp '2024-01-01');

create table if not exists orders_partitioned_2024 partition of orders_partitioned
for values from (timestamp '2024-01-01') to (timestamp '2025-01-01');

create table if not exists orders_partitioned_2025 partition of orders_partitioned
for values from (timestamp '2025-01-01') to (timestamp '2026-01-01');

create table if not exists orders_partitioned_default partition of orders_partitioned default;

-- преимущества: при where по created_at читаются только нужные партиции (partition pruning), меньше строк/страниц.
-- insert/update: insert попадает в нужную партицию; update с изменением created_at может переносить строку между партициями.
-- быстрее: запросы с фильтром по created_at (диапазоны дат).
-- может быть медленнее: запросы без условия по created_at (придётся смотреть много партиций), если нет доп. индексов.