Вже згадував про можливості
EXPLAIN
в пості PostgreSQL: AWS RDS Performance and monitoring, але це настільки цікава і корисна штука, що варто про неї поговорити окремо.
До того ж, в AWS RDS для PostgreSQL є можливість включити логування Execution Plans з EXPLAIN
, що теж корисно для моніторингу і дебагу, тому подивимось як це включити і які параметри для цього є.
Зміст
PostgreSQL EXPLAIN
Отже, що таке EXPLAIN? див. також Using EXPLAIN.
Explain відобразить нам Execution Plan. А аби краще розуміти що таке Execution Plan – глянемо на те, як взагалі виглядає процес виконання запитів.
Що таке Execution Plan в PostgreSQL
Коли ми відправляємо запит до PostgreSQL, то:
- запит проходить через лексичний (Lexing) та синтаксичний (Parsing) аналіз
- Lexing: розділення на токени (оператори) –
SELECT
,FROM
,WHERE
тощо - Parsing: перевірка синтаксису та створення дерева синтаксичного аналізу (Parse Tree)
- Lexing: розділення на токени (оператори) –
- далі відбувається аналіз (Analysis/Binding)
- перевірка існування таблиць, колонок та функцій, прав доступу
- створюється схематична інформація (schema information) – мета-дані про структуру таблиць, індекси, привілегії тощо
- створюється логічний план (Logical Plan Generation)
- на основі дерева синтаксичного аналізу (Parse Tree) формується логічний план запиту, в якому визначаються операції, які треба виконати –
JOIN
,AGREGATE
etc
- на основі дерева синтаксичного аналізу (Parse Tree) формується логічний план запиту, в якому визначаються операції, які треба виконати –
- виконується оптимізація запиту (Query Optimization)
- визначаються найкращі алгоритми
JOIN
(Nested Loop
,Hash Join
,Merge Join
) - вирішується, чи використовувати індекси (
Index Scan
,Bitmap Index Scan
) або повний перегляд таблиці (Seq Scan
)
- визначаються найкращі алгоритми
- генерується фізичний план (Execution Plan Generation)
- логічний план перетворюється у конкретний набір операцій для виконання (
Seq Scan
,Index Scan
,Hash Join
тощо) - визначається, як буде використовуватися оперативна пам’ять, кеш і тимчасові файли на диску (саме тому фізичний план – бо вже враховуються фізичні ресурси системи)
- логічний план перетворюється у конкретний набір операцій для виконання (
- і нарешті відбувається виконання запиту (Execution)
- отримуємо дані з диску, змінюємо дані в пам’яті тощо
- якщо це
SELECT
– дані відправляються клієнту - якщо виконується
INSERT
/UPDATE
/DELETE
– дані фізично змінюються на диску
- завершення операції (Commit або Rollback)
COMMIT
(підтвердження транзакції) – зміни фіксуються в базі даних, і стають доступні іншим клієнтамROLLBACK
(скасування транзакції) – зміни повертаються до стану, який був доBEGIN
Використання EXPLAIN
Отже, з EXPLAIN
ми можемо отримати Execution Plan – тобто, побачити те, які операції будуть виконуватись, і скільки даних і ресурсів вони торкнуться.
Найпростіший приклад може виглядати так:
EXPLAIN SELECT * FROM table_name;
Execution Plan являє собою дерево фізичних операцій, де Nodes – це окрема операція, така як Seq Scan
, Index Scan
, Sort
, Join
, Aggregate
тощо, а стрілочки – зв’язки між вузлами і передача даних між операціями.
Візуалізація EXPLAIN
Є можливість візуалізувати результати Explain, наприклад онлайн з https://www.pgexplain.dev:
Або https://explain.depesz.com:
Опції EXPLAIN
Загальний синтаксис:
EXPLAIN (options) statement
Основні Options такі:
ANALYZE
: коли ми просто викликаємоEXPLAIN
, то реальні дії не виконуються; додаванняANALYZE
запускає виконання запиту, і потім відображає статистику- default: FALSE
VERBOSE
: включає в результат додаткову інформацію (імена колонок, функцій тощо)- default: FALSE
COSTS
: відображення “вартості” виконання операції – умовна одиниця вимірювання витрат на виконання запиту з врахування затрат CPU та IO- default: TRUE
BUFFERS
: відображення використання буферів пам’яті (тільки якщоANALYZE=TRUE
) – скільки даних знайдено в кеші, скільки буде зчитано з диску, записано на диск тощо- default: FALSE
FORMAT
: в якому форматі вивести результат –TEXT
,XML
,JSON
,YAML
- default: TEXT
Результати EXPLAIN
Давайте глянемо, що у нас було в результаті виконання такого запиту:
EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT YAML) SELECT * FROM foster_home;
- основний план і загальна інформація:
Node Type: "Seq Scan"
: операція послідовного сканування (Sequential Scan), без використання індексівParallel Aware: false
: запит буде виконано без паралелізаціїAsync Capable: false
: запит не може бути виконано асинхронноRelation Name: "foster_home"
: ім’я таблиці, над якою виконуються діїSchema: "public"
: ім’я схеми, в якій знаходиться таблиця
- оцінка вартості (COSTS):
Startup Cost: 0.00
: операція починається миттєво, оскільки це послідовне читанняTotal Cost: 16242.01
: оцінка вартості всієї операції (не час, а умовні одиниці)Plan Rows: 35801
: планується повернути в результаті запита 35801 рядківPlan Width: 1330
: середній очікуваний розмір рядка в байтах
- фактичне виконання запиту (ANALYZE):
Actual Startup Time: 0.018
: час на початок виконання запитуActual Total Time: 638.911
: скільки всього часу зайняло виконанняActual Rows: 35801
: скільки рядків було повернуто в результаті виконання
- вихідні дані (Output):
- список колонок, які були повернуті в результаті запиту (всі, бо виконуємо
SELECT *
)
- список колонок, які були повернуті в результаті запиту (всі, бо виконуємо
- буфери пам’яті (BUFFERS):
Shared Hit Blocks: 455
: вshared_buffers
було знайдено 455 блоків з існуючими данимиShared Read Blocks: 15429
: 15429 блоків довелось читати з дискуShared Dirtied Blocks: 0
таShared Written Blocks: 0
: в результаті виконання запиту ніякі дані не змінювалисьTemp Read Blocks: 0
таTemp Written Blocks: 0
– тимчасові файли не використовувались (temp_blks_read
вpg_stat_statements
)
- операції введення/виведення (I/O)
I/O Read Time: 599.188
: скільки часу витратили на читання з дискуI/O Write Time: 0.000
: нічого не писалиTemp I/O Read Time: 0.000
таTemp I/O Write Time: 0.000
: знов-таки, тимчасові файли не використовувались
EXPLAIN
та використання пам’яті
Що найбільш цікаве для мене, бо до цього всього ми прийшли саме тому, що розбирались з питанням “що з’їло всю пам’ять на сервері” – це скільки пам’яті буде використано при виконання різних запитів.
Використання shared_buffers
Отже, в результатах EXPLAIN
ми бачили, що оцінка була:
Plan Rows: 35801
: планується повернути в результаті запита 35801 строкPlan Width: 1330
: і кожен рядок займе 1330 байт у пам’яті
Тобто, в результаті виконання запиту SELECT * FROM foster_home
очікується отримати 35801 рядків, в середньому кожен розміром в 1330 байт.
Відповідно, в shared_buffers
на час виконання запиту планується мати:
35801*1330/1024/1024 45
45 мегабайт.
Але реальний розмір після виконання вказано в Shared Hit Blocks
+ Shared Read Blocks
: 455 блоків даних по 8 КБ вже було в пам’яті, і 15429 блоків було зчитано з диску.
Враховуючи, що кожен блок це 8КБ, то маємо:
(455+15429)*8/1024 124
Порівняємо з розміром самої таблиці:
dev_kraken_db=> SELECT pg_size_pretty(pg_relation_size('foster_home')); pg_size_pretty ---------------- 124 MB
Отже, в результаті виконання SELECT * FROM foster_home
ми в shared_buffers
будемо мати 124 мегабайти (3640 байти там вже були, і 123432 буде додатково зчитано з диску).
При тому, що під shared_buffers
на Dev-сервері всього виділено:
dev_kraken_db=> SHOW shared_buffers; shared_buffers ---------------- 190496kB
~186 мегабайт.
Різниця в Plan Width і Shared Read Blocks
Чому в плані ми бачили ~45 мегабайт, а після виконання – 124 MB?
Бо Plan Width
відображає дані по середньому розміру рядка, а Shared Read Blocks
– скільки фізичних блоків даних було прочитано з диску.
Для значень в Plan Width
PostgreSQL використовує дані з view pg_stats.avg_width
.
Колонки в нашій таблиці:
dev_kraken_db=> \d foster_home Table "public.foster_home" Column | Type | Collation | Nullable | Default ---------------------+-------+-----------+----------+--------- challenge_id | text | | | type | text | | | parent_challenge_id | text | | | source_challenge_id | text | | | base_challenge_id | text | | | bio_parent_id | text | | | matched_by | text | | | title | text | | | challenge | jsonb | | | user_id | text | | | Indexes: "foster_home_challenge_id" btree (challenge_id)
І їх розмір в pg_stats
:
dev_kraken_db=> SELECT attname, avg_width FROM pg_stats WHERE tablename = 'foster_home'; attname | avg_width ---------------------+----------- type | 7 bio_parent_id | 37 matched_by | 18 challenge_id | 37 parent_challenge_id | 37 title | 27 user_id | 37 source_challenge_id | 37 base_challenge_id | 37 challenge | 1056
Що дає нам ті самі 1330 байт:
1056+37+37+37+27+37+37+18+37+7 1330
Отже, планується отримати 35801 строк, і кожна приблизно ~1330 байт.
Але реальний їх розмір ми можемо дізнатись з pg_column_size()
:
dev_kraken_db=> SELECT pg_size_pretty(avg(pg_column_size(t))) FROM foster_home t; pg_size_pretty ----------------------------- 1424.0065081980950253 bytes
Або:
1424*35801/1024/1024 48
48 мегабайт.
Але ж все одно не 124 мегабайти, які були прочитані з диска? Чому так?
Бо:
- дані з диску читаються блоками по 8 КБ, а не окремими рядками:
- тобто, навіть якщо треба отримати кілька рядків в пару кілобайт, то для їх отримання доведеться зчитати 8 КБ блоку, в якому вони знаходяться
- таблиця може бути фрагментованою:
- тобто рядки можуть знаходиться в різних блоках даних на диску, і читати доведеться кожен блок
- виконується
Seq Scan
(повне сканування таблиці), а не використовується індекс:- через це з диску читається вся таблиця
- частина даних може зберігатись поза основною таблицею в TOAST-таблиці:
- але в
SELECT pg_size_pretty(pg_relation_size('foster_home'));
ми вже маємо розмір без TOAST та індексів – 124 мегабайти - отримати весь розмір можна з
SELECT pg_size_pretty(pg_total_relation_size('foster_home'));
- але в
Відносно індексу – як виглядає різниця в читані даних, коли індекс є, і коли нема.
Наприклад, подивимось EXPLAIN SELECT
по полю user_id
, для якого нема індексу:
dev_kraken_db=> EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT YAML) SELECT user_id FROM foster_home; ... Node Type: "Seq Scan" + ... Total Cost: 16242.01 + Plan Rows: 35801 + Plan Width: 37 + ... Actual Rows: 35801 + ... Output: + - "user_id" + Shared Hit Blocks: 715 + Shared Read Blocks: 15169 + ...
Знов маємо Node Type: "Seq Scan"
, тобто вичитування всієї таблиці з диску і, відповідно, багато Shared Read Blocks
.
Тоді як для challenge_id
, для кого індекс є, це було б:
dev_kraken_db=> EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT YAML) SELECT challenge_id FROM foster_home; QUERY PLAN -------------------------------------------- - Plan: + Node Type: "Index Only Scan" + ... Total Cost: 1629.43 + Plan Rows: 35801 + Plan Width: 37 + ... Actual Rows: 35801 + ... Output: + - "challenge_id" + Heap Fetches: 0 + Shared Hit Blocks: 271 + Shared Read Blocks: 0 + ...
Тут всі дані з нашого індексу вже є в shared_buffers
, тому з диску взагалі нічого не читалось.
Перевірити shared_bufers
для індексу foster_home_challenge_id
можемо так:
dev_kraken_db=> SELECT count(*) AS cached_pages, pg_size_pretty(count(*) * 8192) AS cached_size FROM pg_buffercache WHERE relfilenode = (SELECT relfilenode FROM pg_class WHERE relname = 'foster_home_challenge_id'); cached_pages | cached_size --------------+------------- 271 | 2168 kB
Окей – тут наче розібрались?
Але окрім shared_buffers
під час виконання запиту може ще використовуватись і work_mem
.
Використання work_mem
work_mem
використовується, якщо в запиті є ORDER BY
, HASH JOIN
.
Див. Resource Consumption.
При цьому якщо в одному запиті є кілька таких операцій – то під кожну буде виділено свою область work_mem
:
Query Plan: Sort (work_mem = 4MB) -> Nested Loop -> Sort (work_mem = 4MB) -> Hash Join (work_mem = 4MB)
Крім того, окремий work_mem
виділяється кожному Worker, тобто додатковому процесу при виконанні запиту – див. How Parallel Query Works:
... -> Sort (cost=25736.77..25774.06 rows=14917 width=1330) (actual time=1286.099..1352.484 rows=11934 loops=3) ... Worker 0: actual time=1279.232..1338.547 rows=10752 loops=1 ... Worker 1: actual time=1281.125..1358.600 rows=12929 loops=1 ... ...
Перевірити, скільки work_mem
споживає запит, і чи достатньо дефолтних 4 мегабайт можемо так:
dev_kraken_db=> EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT YAML) dev_kraken_db-> SELECT * FROM foster_home ORDER BY title; QUERY PLAN -------------------------------------------- - Plan: + Node Type: "Gather Merge" + ... Temp Read Blocks: 5497 + Temp Written Blocks: 5507 + ... Temp I/O Read Time: 192.337 + Temp I/O Write Time: 117.445 + ... Plans: + - Node Type: "Sort" ... Sort Key: + - "foster_home.title" + Sort Method: "external merge" + Sort Space Used: 17048 + Sort Space Type: "Disk" + ...
Тут ми бачимо, що:
Temp Read
таWritten Blocks
: використовувались тимчасові блокиTemp I/O Read
таWrite Time
: був витрачений час на роботу з дискомSort Method: "external merge"
таSort Space Type: "Disk"
: для сортування використовувався диск, а не"Memory"
, бо вwork_mem
весь результат не вмістивсяSort Space Used: 17048
: для сортування знадобилось 17048 кілобайт (16 мегабайт)
Перевірити поточне значення для work_mem
можемо так:
dev_kraken_db=> SHOW work_mem; work_mem ---------- 4MB
Саме тому для сортування знадобилось створювати temp files.
Якщо ж ми збільшимо work_mem
(через SET
для поточної сесії, або через Parameters Group для RDS аби зміни були постійними):
dev_kraken_db=> SET work_mem = '20MB'; SET
То тепер будемо бачити іншу картину:
... Temp Written Blocks: 0 + ... Temp I/O Write Time: 0.000 + ... Sort Key: + - "foster_home.title" + Sort Method: "quicksort" + Sort Space Used: 17454 + Sort Space Type: "Memory" + ...
Отже, як резюме: з EXPLAIN ANALYZE
ми можемо отримати інформацію про те, скільки даних при виконанні запиту буде зчитано з диску в shared_buffers
, і скільки даних буде використано в work_mem
або temp files
Як нам це може допомогти моніторити “важкі” запити в AWS RDS?
AWS RDS PostgreSQL та auto_explain
В RDS ми можемо включити auto_explain
, який буде записувати в лог результат EXPLAIN для подальшого аналізу.
Документація – auto_explain — log execution plans of slow queries. Код самого модуля – auto_explain.c.
Аби включити auto_explain
в RDS, його треба додати в shared_preload_libraries
:
Після чого ребутаємо інстанс RDS.
Далі налаштовуємо такі параметри:
auto_explain.log_min_duration
: скільки часу має виконуватись запит, аби бути записаним в лог зEXPLAIN
(в мілісекундах)auto_explain.log_analyze
: виконуватиANALIZE
, а не простоEXPLAIN
(може впливати на навантаження RDS)auto_explain.log_buffers
: додавати інформацію поBUFFERS
auto_explain.log_verbose
:VERBOSE
дляEXPLAIN
auto_explain.log_format
: в якому форматі пишемо лог
Зберігаємо параметри, і перевіряємо лог:
Готово.
Корисні посилання
- Reading a Postgres EXPLAIN ANALYZE Query Plan
- What Is an Execution Plan and How to Find It in PostgreSQL
- Execution Plan
- System Administration Functions
- auto_explain — log execution plans of slow queries
- How can I log execution plans of queries for Amazon RDS PostgreSQL or Aurora PostgreSQL to tune query performance?
- Postgres auto_explain: Log Slow PostgreSQL Query Plans Automatically
- Common DBA tasks for Amazon RDS for PostgreSQL
- Lexical Scanning in PostgreSQL