From 3692b5625dff3554b21087bfdef279abb605446f Mon Sep 17 00:00:00 2001 From: plidan123 Date: Sat, 20 Dec 2025 03:58:15 +0300 Subject: [PATCH] Add files via upload --- lab_14.sql | 387 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 lab_14.sql diff --git a/lab_14.sql b/lab_14.sql new file mode 100644 index 0000000..bfce8a9 --- /dev/null +++ b/lab_14.sql @@ -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 (будут читаться только нужные партиции). + +-- Задание 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 (придётся смотреть много партиций), если нет доп. индексов. \ No newline at end of file