2. Что за зверь EXPLAIN?
2
EXPLAIN - показывает какой план выполнения запроса был выбран планировщиком.
Планировщик решает задачу оптимизации времени выполнения запроса.
EXPLAIN ANALYZE - реально выполняет запрос и дополняет вывод EXPLAIN реальными метриками
4. Sequence Scan
4
План запроса
Seq Scan on users u (cost=0.00..118835.16 rows=373716 width=3260) (actual time=0.009..460.220 rows=372892 loops=1)
Planning Time: 0.939 ms
Execution Time: 474.262 ms
EXPLAIN ANALYZE select * from users u;
5. Метрики EXPLAIN
5
cost - приблизительная стоимость запуска и общая стоимость. Общая стоимость вычисляется по формуле:
(число_чтений_диска * seq_page_cost) + (число_просканированных_строк * cpu_tuple_cost)
rows - ожидаемое число строк, которое должен вывести узел плана
width - ожидаемый средний размер строк, выводимых узлом плана (в байтах)
6. Sequence Scan
6
План запроса
Gather (cost=1000.00..118045.61 rows=14 width=2761) (actual time=0.906..48.498 rows=141 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on users u (cost=0.00..117044.21 rows=6 width=2761) (actual time=0.767..44.362 rows=47 loops=3)
Filter: ((name)::text = 'Дима'::text)
Rows Removed by Filter: 124229
Planning Time: 0.168 ms
Execution Time: 48.549 ms
EXPLAIN ANALYZE select * from users u where name = 'Дима';
Filter не влияет на общую стоимость запроса
7. Sequence Scan
7
План запроса
Limit (cost=1000.00..84604.17 rows=10 width=3260) (actual time=0.924..8.483 rows=10 loops=1)
-> Gather (cost=1000.00..118045.84 rows=14 width=3260) (actual time=0.923..8.480 rows=10 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on users u (cost=0.00..117044.44 rows=6 width=3260) (actual time=0.662..4.601 rows=5
loops=3)
Filter: ((name)::text = 'Дима'::text)
Rows Removed by Filter: 16289
Planning Time: 0.183 ms
Execution Time: 8.532 ms
EXPLAIN ANALYZE select * from users u where name = 'Дима' limit 10;
8. Index Scan
8
План запроса
Index Scan using users_pkey on users u (cost=0.42..8.44 rows=1 width=3250) (actual time=0.014..0.015 rows=1 loops=1)
Index Cond: (id = 28)
Planning Time: 1.090 ms
Execution Time: 0.065 ms
EXPLAIN ANALYZE select * from users u where id = 28;
10. Index Scan
10
План запроса
Limit (cost=0.42..13.06 rows=10 width=3275) (actual time=0.018..0.052 rows=10 loops=1)
-> Index Scan using users_pkey on users u (cost=0.42..471916.33 rows=373551 width=3275) (actual time=0.017..0.051
rows=10 loops=1)
Planning Time: 0.169 ms
Execution Time: 0.087 ms
EXPLAIN ANALYZE select * from users u order by id limit 10;
11. Index Scan
11
План запроса
Limit (cost=0.42..40.02 rows=10 width=3275) (actual time=0.012..0.035 rows=10 loops=1)
-> Index Scan using users_pkey on users u (cost=0.42..12406.00 rows=3133 width=3275) (actual time=0.011..0.033
rows=10 loops=1)
Index Cond: (id > 400000)
Planning Time: 0.223 ms
Execution Time: 0.072 ms
EXPLAIN ANALYZE select * from users u where id > 400000 limit 10;
12. Index Scan
12
План запроса
Limit (cost=0.42..0.81 rows=10 width=4) (actual time=0.011..0.015 rows=10 loops=1)
-> Index Only Scan using users_pkey on users u (cost=0.42..14346.36 rows=373551 width=4) (actual time=0.011..0.013
rows=10 loops=1)
Heap Fetches: 0
Planning Time: 0.051 ms
Execution Time: 0.023 ms
EXPLAIN ANALYZE select id from users u limit 10;
13. Bitmap Heap Scan
13
План запроса
Bitmap Heap Scan on users u (cost=347.25..28336.88 rows=9139 width=3250) (actual time=3.924..22.035 rows=8960
loops=1)
Recheck Cond: (created_at >= '2021-08-01 00:00:00+00'::timestamp with time zone)
Heap Blocks: exact=7153
-> Bitmap Index Scan on users_created_at_idx (cost=0.00..344.97 rows=9139 width=0) (actual time=2.978..2.979
rows=9027 loops=1)
Index Cond: (created_at >= '2021-08-01 00:00:00+00'::timestamp with time zone)
Planning Time: 0.155 ms
Execution Time: 22.487 ms
EXPLAIN ANALYZE select * from users u where created_at >= '2021-08-01';
19. Merge Join
19
План запроса
EXPLAIN ANALYZE select * from users u
left join (select * from lessons where is_paid=0 order by student_id) l on u.id=l.student_id
order by u.id limit 100 ;
20. Merge Join
20
EXPLAIN ANALYZE select * from users u
left join (select * from lessons where is_paid=0 order by student_id) l on u.id=l.student_id order by u.id limit 100 ;Limit
(cost=246715.93..246851.60 rows=100 width=4592) (actual time=541.589..552.755 rows=100 loops=1)
-> Merge Right Join (cost=246715.93..753755.40 rows=373727 width=4592) (actual time=541.588..552.746 rows=100 loops=1)
Merge Cond: (lessons.student_id = u.id)
-> Gather Merge (cost=245746.43..274355.33 rows=245202 width=1330) (actual time=541.557..552.607 rows=92 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Sort (cost=244746.41..245052.91 rows=122601 width=1330) (actual time=537.927..537.954 rows=104 loops=3)
Sort Key: lessons.student_id
Sort Method: quicksort Memory: 92752kB
Worker 0: Sort Method: quicksort Memory: 110826kB
Worker 1: Sort Method: quicksort Memory: 91642kB
-> Parallel Seq Scan on lessons (cost=0.00..234384.41 rows=122601 width=1330) (actual time=0.009..366.614 rows=96905 loops=3)
Filter: (is_paid = 0)
Rows Removed by Filter: 607836
-> Index Scan using users_pkey on users u (cost=0.42..472948.71 rows=373727 width=3262) (actual time=0.021..0.045 rows=10 loops=1)
Planning Time: 0.609 ms
23. Unique
23
Limit (cost=264201.52..264202.10 rows=20 width=4602) (actual time=154.776..155.284 rows=1 loops=1)
-> Unique (cost=264201.52..264202.10 rows=20 width=4602) (actual time=154.775..155.281 rows=1 loops=1)
-> Sort (cost=264201.52..264201.81 rows=115 width=4602) (actual time=154.773..155.266 rows=111 loops=1)
Sort Key: u.id, l.created_at
Sort Method: quicksort Memory: 434kB
-> Gather (cost=1000.43..264197.59 rows=115 width=4602) (actual time=80.673..153.885 rows=111 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Nested Loop Left Join (cost=0.43..263186.09 rows=48 width=4602) (actual time=55.822..82.079 rows=37 loops=3)
-> Parallel Seq Scan on users u (cost=0.00..117039.64 rows=8 width=3260) (actual time=55.809..55.811 rows=0 loops=3)
Filter: ((email)::text ~~ 'webdev%'::text)
Rows Removed by Filter: 124361
-> Index Scan using lessons_group_lesson_id_student_id_idx on lessons l (cost=0.43..18267.49 rows=82 width=1330) (actual
time=0.029..78.700 rows=111 loops=1)
Index Cond: (u.id = student_id)
EXPLAIN ANALYZE select DISTINCT ON(u.id) * from users u
left join lessons l on u.id=l.student_id where email like 'webdev%' order by u.id, l.created_at limit 100;
План запроса
24. Заключение
24
Планировщик черпает
данные с таблицы
pg_statistic, но можно
смотреть представление
pg_stats
Инструменты:
pgBadger - формирует
отчеты по логам PostgreSQL
explain.depesz.com -
помагает чиать explain’ы
pgMustard - дает советы как
улучшить запрос на основе
explain
Мы все привыкли к ORM и забыли как выглядят запросы к БД, а если дело доходит до того, что в проде что-то тормозит, и нам даже удалось понять из-за чего, мы начинаем задумываться над тем, чтобы оптимизировать запрос, но с чего начать и как подступиться, на помощь приходит постгрес со своим мощным инструментом анализа запросов EXPLAIN, сегодня собственно и попытаюсь познакомить вас с этим инструментом.
В дебрях PostgreSQL живет планировщик запросов, который решает задачу оптимизации времени выполнения запроса, основываясь на мета-информации (информации о информации), кол-во строк в таблице, кол-во различных значений, наиболее частые данные, для очень больших таблиц он берет случайную выборку и ней строит мета информацию.
EXPLAIN - показывает какой план выполнения запроса был выбран планировщиком. EXPLAIN ANALYZE - реально выполняет запрос и дополняет вывод EXPLAIN реальными метрикамиНадеюсь к концу доклада мы сможем интерпретировать результаты выполенния EXPLAIN
Начнем с методов сканирования таблиц, сканирование это основа для получения данных из таблицы
Как видим запрос довольно дорогой, см. cost, так как потребует большого кол-ва чтений с диска и сканирования строк, так же в одной строке получим 3260 байта, указав конкретные атрибуты уменьшим width
Cost - извлечение одной страницы в последовательном (sequential) порядке
Filter будет применен к каждой отсканированной строке, потому оценка стоимости должна даже возрасти так как нужно проверять, условие в WHERE
Последовательное сканирование закончится очень быстро – сразу как только удовлетворит аппетит LIMIT
Index Scan заключается в том что постгрес открывает индекс, проводит поиск в нем, если находит в нем строки соответствующие условию, то открывает таблицу и получает строки, на которые ссылается индекс.
Для общего понимания остановлюсь как устроены файлы таблиц в постгрес.
Они разбиты на страницы по 8192байт
Страница состоит из таких основных частей: heap tuple(s), line pointers, заголовки.
Для идентификации кортежа используется tuple identifier (TID), который состоит из номера страницы и указателя.
Собственно индексы хранят TID, по которым в последствии и находятся нужные строки в таблице.
Index Scan также используется, когда вы хотите отсортировать какие-то данные, используя порядок сортировки в индексе
Если бы условие было id > 10 то постгрес бы не использовал бы сканирование по индексу так как такому условию соответствуют почти все строки в таблице и поиск по индексу только добавит накладные расходы по получению данных по ссылкам из индекса, будет предпочтительней использовать seq scan
Обратите внимание на слово “Only" в “Index Only Scan".
Постгрес понял, что я выбираю только данные которые хранятся в индексе, потому чтение будет происходть прямо из индекса.
Bitmap Scans всегда состоят, минимум, из двух узлов. Сначала (на нижнем уровне) идет Bitmap Index Scan, а затем – Bitmap Heap Scan.
Этап Bitmap Index Scan строит битовую карту, для каждой страницы в таблицы выделяет один бит, сначала все биты равны 0, по ходу сканирования индекса мы помечаем 1й страницы, в которых есть соответствующие данные из индекса.
После чего на этапе Bitmap Heap Scan постгрес проходится по битовой карте достает нужные страницы и перепроверяет данные в них, выкидывая лишнее.
Такой метод сканирования используется на случай если данные по таблице разбросаны и постоянное чтение по указателю из индекса при обычном Idex Scan будет провоцировать много обращений к диску.
Можно сказать это смесь Idex Scan и Seq Scan.
Перейдем к объединению данных при чтении из нескольких таблиц
Nested Loop состоит из двух узлов, как видно постгрес сканирует таблицу users и для каждой строки делает сканирование index scan для того что бы получить данные из таблицы user_meta, если данные не были найдены строка из users игнорируется. Тут можно заметить, что в сумме сканирование по индексу выполнилось 10 раз, а все остальные оценки это средние значения.
В данном примере планировщик выбрал Hash Right Join, который состоит из 2х частей:
- постгрес сначала сделает Bitmap Heap Scan по users, то есть найдет юзеров по условию u.created_at > date '2021-08-01' и создаст Hash - ассоциативный массив, где ключом будет выступать поле которому происходить объеденение в нашем случае id
- вторым шагом, постгрес запускает seq scan по lessons и смотрит или в hash map есть соответствующий ключ, так как у нас right join то если ключа в hash map нету то исключиться строка из lessons а не из users.
dsdf
Merge Join - используется, если объединяемые наборы данных отсортированы (или могут быть отсортированы с небольшими затратами) с помощью ключа join.
dsdf
sort берет выбранные записи и возвращает их отсортированными определенным образом, если память, требующаяся для сортировки, будет больше, чем значение work_mem, то произойдет переключение на дисковую сортировкуHashAggregate - схоже к работе Hash объеденения, в данном случае по ключу reg_country_code формируем ассоциативный массив, дальше есть возможность применить функцию агрегации к каждой “корзине” ассоциативного массива.
Для Unique необходимо, что бы данные были отсортированны, это нужно для того. что бы строки с одинаковым значением находились рядом и могли быть схлопнуты, Unique практически не требует памяти. Она просто сравнивает значение в предыдущей строке с текущим и, если они одинаковые, отбрасывает его. Вот и всё.
Сегодня познакомились с основными частями в Explain, но конечно можно углубляться дальше, а именно смотреть на таблицу pg_statistic или более человек понятное представление pg_stats, чтобы понять на основе чего планировщик делает вывод какой план построить.
Так же хочу поделиться инструментами которые помагают при анализе запросов Postgresql.