-
Notifications
You must be signed in to change notification settings - Fork 0
lab_14 #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
plidan123
wants to merge
1
commit into
main
Choose a base branch
from
lab_14
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
lab_14 #25
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 (будут читаться только нужные партиции). | ||
|
|
||
| -- Задание 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 (придётся смотреть много партиций), если нет доп. индексов. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍