ANALYZE aircrafts;ANALYZE
В качестве примера ситуации, в которой оптимизация запроса представляется обос- нованной, рассмотрим следующую задачу. Предположим, что необходимо опреде- лить степень загруженности кассиров нашей авиакомпании в сентябре 2016 г. Для этого, в частности, требуется выявить распределение числа операций бронирования по числу билетов, оформленных в рамках этих операций. Другими словами, это озна- чает, что нужно подсчитать число операций бронирования, в которых был оформлен только один билет, число операций, в которых было оформлено два билета и т. д.
Эту задачу можно переформулировать так: для каждой строки, отобранной из табли- цы «Бронирования» (bookings), нужно подсчитать соответствующие строки в таб- лице «Билеты» (tickets). Речь идет о строках, в которых значение поля book_ref такое же, что и в текущей строке таблицы bookings. Буквальное следование такой формулировке задачи приводит к получению запроса с коррелированным подзапро- сом в предложении SELECT. Но это еще не окончательное решение. Теперь нужно сгруппировать полученный набор строк по значениям числа оформленных билетов.
312
10.5. Оптимизация запросов
Получаем такой запрос:
EXPLAIN
SELECT num_tickets, count( * ) AS num_bookings
FROM
( SELECT b.book_ref,
( SELECT count( * ) FROM tickets t
WHERE t.book_ref = b.book_ref
)
FROM bookings b
WHERE date_trunc( 'mon', book_date ) = '2016-09-01'
) AS count_tickets( book_ref, num_tickets )
GROUP by num_tickets
ORDER BY num_tickets DESC;
QUERY PLAN
-----------------------------------------------------------------------
GroupAggregate (cost=14000017.12..27994373.35 rows=1314 width=16)
Group Key: ((SubPlan 1))
-> Sort (cost=14000017.12..14000020.40 rows=1314 width=8)
Sort Key: ((SubPlan 1)) DESC
-> Seq Scan on bookings b
(cost=0.00..13999949.05 rows=1314 width=8)
Filter: (date_trunc('mon'::text, book_date) = '2016-09-01 00:00:00+08'::timestamp with time zone)
SubPlan 1
-> Aggregate (cost=10650.17..10650.18 rows=1 width=8)
-> Seq Scan on tickets t
(cost=0.00..10650.16 rows=2 width=0)
Filter: (book_ref = b.book_ref)
(10 строк)
В плане получены очень большие оценки общей стоимости выполнения запроса:
cost=14000017.12..27994373.35
Универсальной зависимости между оценкой стоимости и реальным временем вы- полнения запроса не существует. Не всегда можно даже приблизительно предполо- жить, в какие затраты времени выльется та или иная оценка стоимости. Но, тем не менее, при рассмотрении других запросов оценок такого порядка нам еще не встре- чалось. Планировщик предполагает, что из таблицы tickets в подзапросе будет извлекаться всего по две строки, и эту операцию нужно будет проделать 1 314 раз:
столько строк предположительно будет выбрано из таблицы bookings. Как видно
313
Глава 10. Повышение производительности
из плана, для просмотра строк в таблице tickets используется ее последовательное сканирование. В результате оценка стоимости этого узла плана получается высокой:
cost=0.00..10650.16
Если у вас не очень мощный компьютер, то время получения результата может выйти за разумные пределы, и вам придется прервать процесс с помощью клавиш
<
Ctrl>+.
Что можно сделать для ускорения выполнения запроса? Давайте создадим индекс для таблицы tickets по столбцу book_ref, по которому происходит поиск в ней.
CREATE INDEX tickets_book_ref_key
ON tickets ( book_ref );
CREATE INDEX
Повторим запрос, добавив параметр ANALYZE в команду EXPLAIN. Новый план, в ко- тором отражены и фактические результаты, будет таким:
QUERY PLAN
-------------------------------------------------------------------------
GroupAggregate (cost=22072.70..38484.52 rows=1314 width=16)
(actual time=3656.554..3787.562 rows=5 loops=1)
Group Key: ((SubPlan 1))
-> Sort (cost=22072.70..22075.99 rows=1314 width=8)
(actual time=3656.533..3726.969 rows=165534 loops=1)
Sort Key: ((SubPlan 1)) DESC
Sort Method: external merge Disk: 2912kB
-> Seq Scan on bookings b (cost=0.00..22004.64 rows=1314 width=8)
(actual time=0.219..3332.162 rows=165534 loops=1)
Filter: (date_trunc('mon'::text, book_date) = '2016-09-01 00:00:00+08'::timestamp with time zone)
Rows Removed by Filter: 97254
SubPlan 1
-> Aggregate (cost=12.46..12.47 rows=1 width=8)
(actual time=0.016..0.016 rows=1 loops=165534)
-> Index Only Scan using tickets_book_ref_key on tickets t
(cost=0.42..12.46 rows=2 width=0)
(actual time=0.013..0.014 rows=1 loops=165534)
Index Cond: (book_ref = b.book_ref)
Heap Fetches: 230699
Planning time: 0.290 ms
Execution time: 3788.690 ms
(15 строк)
314
10.5. Оптимизация запросов
Теперь планировщик использует индекс для поиска в таблице tickets. Причем это поиск исключительно по индексу (Index Only Scan), поскольку нас интересует толь- ко число строк — count( * ), а не их содержание. Обратите внимание на различие предполагаемого и фактического числа извлекаемых строк. Тем не менее запрос стал выполняться значительно — на порядок — быстрее.
Результат имеет такой вид:
num_tickets | num_bookings
-------------+--------------
5 |
13 4 |
536 3 |
7966 2 |
47573 1 |
109446
(5 строк)
Кроме создания индекса есть и другой способ: замена коррелированного подзапроса соединением таблиц.
EXPLAIN ANALYZE
SELECT num_tickets, count( * ) AS num_bookings
FROM
( SELECT b.book_ref, count( * )
FROM bookings b, tickets t
WHERE date_trunc( 'mon', b.book_date ) = '2016-09-01'
AND t.book_ref = b.book_ref
GROUP BY b.book_ref
) AS count_tickets( book_ref, num_tickets )
GROUP by num_tickets
ORDER BY num_tickets DESC;
QUERY PLAN
------------------------------------------------------------------------
GroupAggregate (cost=16966.67..16978.53 rows=200 width=16)
(actual time=4092.258..4219.477 rows=5 loops=1)
Group Key: count_tickets.num_tickets
-> Sort (cost=16966.67..16969.96 rows=1314 width=8)
(actual time=4092.236..4161.294 rows=165534 loops=1)
Sort Key: count_tickets.num_tickets DESC
Sort Method: external merge Disk: 2912kB
-> Subquery Scan on count_tickets
(cost=16858.57..16898.61 rows=1314 width=8)
(actual time=3176.113..3862.133 rows=165534 loops=1)
315
Глава 10. Повышение производительности
-> GroupAggregate (cost=16858.57..16885.47 rows=1314 width=15)
(actual time=3176.111..3765.157 rows=165534 loops=1)
Group Key: b.book_ref
-> Sort (cost=16858.57..16863.16 rows=1834 width=7)
(actual time=3176.098..3552.334 rows=230699 loops=1)
Sort Key: b.book_ref
Sort Method: external merge Disk: 3824kB
-> Hash Join (cost=5632.24..16759.16 rows=1834 width=7)
(actual time=498.701..1091.509 rows=230699 loops=1)
Hash Cond: (t.book_ref = b.book_ref)
-> Seq Scan on tickets t
(cost=0.00..9733.33 rows=366733 width=7)
(actual time=0.047..170.792 rows=366733 loops=1)
-> Hash (cost=5615.82..5615.82 rows=1314 width=7)
(actual time=498.624..498.624 rows=165534 loops=1)
Buckets: 262144 (originally 2048)
Batches: 2 (originally 1)
Memory Usage: 3457kB
-> Seq Scan on bookings b
(cost=0.00..5615.82 rows=1314 width=7)
(actual time=0.019..267.728 rows=165534 loops=1)
Filter: (date_trunc('mon'::text, book_date) =
'2016-09-01 00:00:00+08'::timestamp with time zone)
Rows Removed by Filter: 97254
Planning time: 2.183 ms
Execution time: 4221.133 ms
(21 строка)
В данном плане используется соединение хешированием (Hash Join). При этом ин- декс по таблице tickets игнорируется: таблица просматривается последовательно
(Seq Scan on tickets t). Время выполнения модифицированного запроса оказыва- ется несколько большим, чем в предыдущем случае, когда в запросе присутствовал коррелированный подзапрос. Таким образом, можно заключить, что для ускорения работы оригинального запроса можно было либо создать индекс, либо модифициро- вать сам запрос, даже не создавая индекса.
Другие методы оптимизации выполнения запросов представлены в разделе «Кон- трольные вопросы и задания». Рекомендуем вам самостоятельно с ними ознакомить- ся и поэкспериментировать.
316
Контрольные вопросы и задания
Контрольные вопросы и задания
Перед выполнением упражнений нужно восстановить измененные значения пара- метров:
SET enable_hashjoin = on;
SET
SET enable_nestloop = on;
SET
1. Как вы думаете, почему при сканировании по индексу оценка стоимости ресур- сов, требующихся для выдачи первых результатов, не равна нулю, хотя исполь- зуется индекс, совпадающий с порядком сортировки?
EXPLAIN_SELECT_total_amount_FROM_bookings_ORDER_BY_total_amount_DESC_LIMIT_5;'>EXPLAIN_SELECT_*_FROM_bookings_ORDER_BY_book_ref;'>EXPLAIN
SELECT *
FROM bookings
ORDER BY book_ref;
QUERY PLAN
----------------------------------------------------------------
Index Scan using bookings_pkey on bookings (cost=0.42..8511.24
rows=262788 width=21)
(1 строка)
2. Как вы думаете, если в запросе присутствует предложение ORDER BY, и создан индекс по тем столбцам, которые фигурируют в предложении ORDER BY, то все- гда ли будет использоваться этот индекс или нет? Почему? Проверьте ваши предположения с помощью команды EXPLAIN.
3. Самостоятельно выполните команду EXPLAIN для запроса, содержащего общее табличное выражение (CTE). Посмотрите, на каком уровне находится узел пла- на, отвечающий за это выражение, как он оформляется. Учтите, что общие таб- личные выражения всегда материализуются, т. е. вычисляются однократно и результат их вычисления сохраняется в памяти, а затем все последующие об- ращения в рамках запроса направляются уже к этому материализованному ре- зультату.
4. Прокомментируйте следующий план, попробуйте объяснить значения всех его узлов и параметров.
317
Глава 10. Повышение производительности
EXPLAIN
SELECT total_amount
FROM bookings
ORDER BY total_amount DESC
LIMIT 5;
QUERY PLAN
----------------------------------------------------------------
Limit (cost=8666.69..8666.71 rows=5 width=6)
-> Sort (cost=8666.69..9323.66 rows=262788 width=6)
Sort Key: total_amount DESC
-> Seq Scan on bookings (cost=0.00..4301.88 rows=262788
width=6)
(4 строки)
5. В подавляющем большинстве городов только один аэропорт, но есть и такие го- рода, в которых более одного аэропорта. Давайте их выявим.
EXPLAIN
SELECT city, count( * )
FROM airports
GROUP BY city
HAVING count( * ) > 1;
QUERY PLAN
-----------------------------------------------------------------
HashAggregate (cost=3.82..4.83 rows=101 width=25)
Group Key: city
Filter: (count(*) > 1)
-> Seq Scan on airports (cost=0.00..3.04 rows=104 width=17)
(4 строки)
Для подсчета числа аэропортов в городах используется последовательное ска- нирование и формирование хеш-таблицы (HashAggregate), ключом которой является столбец city. Затем из нее отбираются те записи, значения которых соответствуют условию
Filter: (count(*) > 1)
Как вы думаете, чем можно объяснить, что вторая оценка стоимости в парамет- ре cost для узла Seq Scan, равная 3,04, не совпадает с первой оценкой стоимо- сти в параметре cost для узла HashAggregate?
318
Контрольные вопросы и задания
6. Выполните команду EXPLAIN для запроса, в котором использована какая- нибудь из оконных функций. Найдите в плане выполнения запроса узел с име- нем WindowAgg. Попробуйте объяснить, почему он занимает именно этот уро- вень в плане.
7. Проанализируйте план выполнения операций вставки и удаления строк. При- чем сделайте это таким образом, чтобы данные в таблицах фактически измене- ны не были.
8.* Замена коррелированного подзапроса соединением таблиц является одним из способов повышения производительности.
Предположим, что мы задались вопросом: сколько маршрутов обслуживают са- молеты каждого типа? При этом нужно учитывать, что может иметь место такая ситуация, когда самолеты какого-либо типа не обслуживают ни одного марш- рута. Поэтому необходимо использовать не только представление «Маршруты»
(routes), но и таблицу «Самолеты» (aircrafts).
Это первый вариант запроса, в нем используется коррелированный подзапрос.
EXPLAIN ANALYZE
SELECT a.aircraft_code AS a_code,
a.model,
( SELECT count( r.aircraft_code )
FROM routes r
WHERE r.aircraft_code = a.aircraft_code
) AS num_routes
FROM aircrafts a
GROUP BY 1, 2
ORDER BY 3 DESC;
А в этом варианте коррелированный подзапрос раскрыт и заменен внешним соединением:
EXPLAIN ANALYZE
SELECT a.aircraft_code AS a_code,
a.model,
count( r.aircraft_code ) AS num_routes
FROM aircrafts a
LEFT OUTER JOIN routes r
ON r.aircraft_code = a.aircraft_code
GROUP BY 1, 2
ORDER BY 3 DESC;
319