Ускорение запросов с ORDER BY и LIMIT в плане выполнения в PostgreSQL
Попался такой запрос, использующий временные файлы:
SELECT "core_contractobject"."id",
"core_contractobject"."contract_id",
"core_contractobject"."house_id",
"core_contract"."id",
"core_contract"."user_id",
"auth_user"."id",
"auth_user"."username",
"core_erphouse"."id",
"core_erphouse"."external_id"
FROM "core_contractobject"
JOIN "core_contract" ON "core_contractobject"."contract_id" = "core_contract"."id"
JOIN "auth_user" ON "core_contract"."user_id" = "auth_user"."id"
JOIN "core_erphouse" ON "core_contractobject"."house_id" = "core_erphouse"."id"
WHERE ("core_contract"."isp_org_id" IN (77, 310, 311)
AND ("core_contract"."billing_id" = 'rb'
OR "core_contract"."is_fake_flag" = true)
AND "auth_user"."last_login" IS NOT NULL
AND "core_erphouse"."external_id" IS NOT NULL
AND "core_contractobject"."is_active" = true
AND NOT ("core_erphouse"."external_id" = ''
AND "core_erphouse"."external_id" IS NOT NULL)
AND NOT ("core_erphouse"."external_id" IS NULL))
ORDER BY "core_contractobject"."date_created" DESC
LIMIT 1000 OFFSET 122000;
План выполнения выглядит следующим образом:
smart_house_prod=# EXPLAIN ANALYZE SELECT "core_contractobject"."id",
"core_contractobject"."contract_id",
"core_contractobject"."house_id",
"core_contract"."id",
"core_contract"."user_id",
"auth_user"."id",
"auth_user"."username",
"core_erphouse"."id",
"core_erphouse"."external_id"
FROM "core_contractobject"
JOIN "core_contract" ON "core_contractobject"."contract_id" = "core_contract"."id"
JOIN "auth_user" ON "core_contract"."user_id" = "auth_user"."id"
JOIN "core_erphouse" ON "core_contractobject"."house_id" = "core_erphouse"."id"
WHERE ("core_contract"."isp_org_id" IN (77, 310, 311)
AND ("core_contract"."billing_id" = 'rb'
OR "core_contract"."is_fake_flag" = true)
AND "auth_user"."last_login" IS NOT NULL
AND "core_erphouse"."external_id" IS NOT NULL
AND "core_contractobject"."is_active" = true
AND NOT ("core_erphouse"."external_id" = ''
AND "core_erphouse"."external_id" IS NOT NULL)
AND NOT ("core_erphouse"."external_id" IS NULL))
ORDER BY "core_contractobject"."date_created" DESC
LIMIT 1000 OFFSET 122000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=180999.91..181002.41 rows=1000 width=53) (actual time=4878.602..4878.774 rows=1000 loops=1)
-> Sort (cost=180694.91..181335.71 rows=256320 width=53) (actual time=4860.597..4874.384 rows=123000 loops=1)
Sort Key: core_contractobject.date_created DESC
Sort Method: external merge Disk: 19144kB
-> Gather (cost=1000.43..157743.63 rows=256320 width=53) (actual time=0.679..4437.856 rows=293634 loops=1)
Workers Planned: 3
Workers Launched: 3
-> Nested Loop (cost=0.43..131111.63 rows=82684 width=53) (actual time=0.285..4492.743 rows=73408 loops=4)
-> Nested Loop (cost=0.00..77498.26 rows=82731 width=42) (actual time=0.182..3707.530 rows=73924 loops=4)
-> Nested Loop (cost=0.00..63072.75 rows=102028 width=28) (actual time=0.117..2423.461 rows=90565 loops=4)
-> Parallel Seq Scan on core_contractobject (cost=0.00..25518.92 rows=154746 width=20) (actual time=0.025..658.361 rows=119931 loops=4)
Filter: is_active
Rows Removed by Filter: 112
-> Index Scan using dba_core_contract_hash_id on core_contract (cost=0.00..0.24 rows=1 width=8) (actual time=0.011..0.011 rows=1 loops=479723)
Index Cond: (id = core_contractobject.contract_id)
Rows Removed by Index Recheck: 0
Filter: ((((billing_id)::text = 'rb'::text) OR is_fake_flag) AND (isp_org_id = ANY ('{77,310,311}'::integer[])))
Rows Removed by Filter: 0
-> Index Scan using dba_auth_user_hash_id on auth_user (cost=0.00..0.14 rows=1 width=14) (actual time=0.010..0.010 rows=1 loops=362260)
Index Cond: (id = core_contract.user_id)
Rows Removed by Index Recheck: 0
Filter: (last_login IS NOT NULL)
Rows Removed by Filter: 0
-> Index Scan using core_erphouse_id_pkey on core_erphouse (cost=0.43..0.65 rows=1 width=11) (actual time=0.010..0.010 rows=1 loops=295698)
Index Cond: (id = core_contractobject.house_id)
Filter: ((external_id IS NOT NULL) AND (external_id IS NOT NULL) AND (((external_id)::text <> ''::text) OR (external_id IS NULL)))
Rows Removed by Filter: 0
Planning time: 1.408 ms
Execution time: 4904.807 ms
(29 rows)
Судя по строчке Sort Method: external merge Disk: 19144kB
, временные файлы создаются для сортировки данных.
Созданные ранее индесы типа HASH можно использовать только для присоединения таблиц в том порядке, в каком они соединялись до их создания. Подумал о том, что при наличии соответствующих индексов планировщик мог бы соединять таблицы в другом порядке. Создал соответствующие HASH-индексы, чтобы посмотреть, воспользуется ли планировщик какими-нибудь из них. И действительно, планировщик вместо индекса dba_core_contract_hash_id
планировщик решил воспользоваться индексом dba_core_contractobject_hash_contract_id
для соединения таблиц core_contract
и core_contractobject
в обратном порядке:
smart_house_prod=# EXPLAIN ANALYZE SELECT "core_contractobject"."id",
"core_contractobject"."contract_id",
"core_contractobject"."house_id",
"core_contract"."id",
"core_contract"."user_id",
"auth_user"."id",
"auth_user"."username",
"core_erphouse"."id",
"core_erphouse"."external_id"
FROM "core_contractobject"
JOIN "core_contract" ON "core_contractobject"."contract_id" = "core_contract"."id"
JOIN "auth_user" ON "core_contract"."user_id" = "auth_user"."id"
JOIN "core_erphouse" ON "core_contractobject"."house_id" = "core_erphouse"."id"
WHERE ("core_contract"."isp_org_id" IN (77, 310, 311)
AND ("core_contract"."billing_id" = 'rb'
OR "core_contract"."is_fake_flag" = true)
AND "auth_user"."last_login" IS NOT NULL
AND "core_erphouse"."external_id" IS NOT NULL
AND "core_contractobject"."is_active" = true
AND NOT ("core_erphouse"."external_id" = ''
AND "core_erphouse"."external_id" IS NOT NULL)
AND NOT ("core_erphouse"."external_id" IS NULL))
ORDER BY "core_contractobject"."date_created" DESC
LIMIT 1000 OFFSET 122000;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=178654.29..178656.79 rows=1000 width=53) (actual time=2889.022..2889.443 rows=1000 loops=1)
-> Sort (cost=178349.29..178990.09 rows=256321 width=53) (actual time=2850.817..2884.118 rows=123000 loops=1)
Sort Key: core_contractobject.date_created DESC
Sort Method: top-N heapsort Memory: 21979kB
-> Gather (cost=1000.43..155397.93 rows=256321 width=53) (actual time=0.569..2496.667 rows=293634 loops=1)
Workers Planned: 3
Workers Launched: 3
-> Nested Loop (cost=0.43..128765.83 rows=82684 width=53) (actual time=0.556..2588.157 rows=73408 loops=4)
-> Nested Loop (cost=0.00..75152.60 rows=82731 width=42) (actual time=0.490..2092.979 rows=73924 loops=4)
-> Nested Loop (cost=0.00..44863.52 rows=110887 width=22) (actual time=0.093..1216.599 rows=119858 loops=4)
-> Parallel Seq Scan on core_contract (cost=0.00..25528.46 rows=136752 width=8) (actual time=0.027..145.599 rows=137579 loops=4)
Filter: ((((billing_id)::text = 'rb'::text) OR is_fake_flag) AND (isp_org_id = ANY ('{77,310,311}'::integer[])))
Rows Removed by Filter: 23166
-> Index Scan using dba_auth_user_hash_id on auth_user (cost=0.00..0.14 rows=1 width=14) (actual time=0.006..0.006 rows=1 loops=550315)
Index Cond: (id = core_contract.user_id)
Rows Removed by Index Recheck: 0
Filter: (last_login IS NOT NULL)
Rows Removed by Filter: 0
-> Index Scan using dba_core_contractobject_hash_contract_id on core_contractobject (cost=0.00..0.26 rows=1 width=20) (actual time=0.006..0.006 rows=1 loops=479430)
Index Cond: (contract_id = core_contract.id)
Rows Removed by Index Recheck: 0
Filter: is_active
-> Index Scan using core_erphouse_id_pkey on core_erphouse (cost=0.43..0.65 rows=1 width=11) (actual time=0.006..0.006 rows=1 loops=295698)
Index Cond: (id = core_contractobject.house_id)
Filter: ((external_id IS NOT NULL) AND (external_id IS NOT NULL) AND (((external_id)::text <> ''::text) OR (external_id IS NULL)))
Rows Removed by Filter: 0
Planning time: 1.291 ms
Execution time: 2893.337 ms
(28 rows)
Что любопытно, при этом изменился и алгоритм сортировки результатов: Sort Method: top-N heapsort Memory: 21979kB
. Как видно, теперь сортировка происходит в оперативной памяти, без использования временных файлов. Не нашёл толкового описания этого алгоритма сортировки. Насколько я понимаю, из в процессе сканирования таблиц в оперативную память помещаются первые 123000 строчек, а последующие строчки либо вытесняют из оперативной памяти уже имеющиеся, либо отбрасываются, так что к концу сканирования в оперативной памяти остаются только последние 123000 строчек из выборки, упорядоченные по полю даты. Дале из этих результатов берутся только 1000 последних строчек и возвращаются в качестве результата.
Таким образом, из всех созданных пробных индексов можно оставить только один dba_core_contractobject_hash_contract_id
:
CREATE INDEX dba_core_contractobject_hash_contract_id ON core_contractobject USING HASH (contract_id);
А использовавшийся прежде индекс dba_core_contract_hash_id
можно удалить:
DROP INDEX dba_core_contract_hash_id;