Ускорение запросов с Hash Join в плане выполнения в PostgreSQL
Попался такой запрос, использующий временные файлы:
SELECT COUNT(*) AS "__count"
FROM "core_contractobject"
INNER JOIN "core_contract" ON ("core_contractobject"."contract_id" = "core_contract"."id")
INNER JOIN "auth_user" ON ("core_contract"."user_id" = "auth_user"."id")
INNER 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)
AND "core_contractobject"."ucams_sync" = false);
План выполнения запроса выглядит следующим образом:
smart_house_prod=# EXPLAIN ANALYZE SELECT COUNT(*) AS "__count"
FROM "core_contractobject"
INNER JOIN "core_contract" ON ("core_contractobject"."contract_id" = "core_contract"."id")
INNER JOIN "auth_user" ON ("core_contract"."user_id" = "auth_user"."id")
INNER 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)
AND "core_contractobject"."ucams_sync" = false);
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=138157.46..138157.47 rows=1 width=8) (actual time=1140.386..1140.386 rows=1 loops=1)
-> Gather (cost=138157.04..138157.45 rows=4 width=8) (actual time=1140.372..1151.151 rows=5 loops=1)
Workers Planned: 4
Workers Launched: 4
-> Partial Aggregate (cost=137157.04..137157.05 rows=1 width=8) (actual time=1091.028..1091.029 rows=1 loops=5)
-> Nested Loop (cost=38681.58..137095.65 rows=24556 width=0) (actual time=1006.280..1091.025 rows=0 loops=5)
-> Nested Loop (cost=38681.15..101594.23 rows=24568 width=4) (actual time=680.063..1089.270 rows=244 loops=5)
-> Hash Join (cost=38680.73..84916.75 rows=30338 width=8) (actual time=676.084..978.456 rows=13409 loops=5)
Hash Cond: (core_contractobject.contract_id = core_contract.id)
-> Parallel Seq Scan on core_contractobject (cost=0.00..45654.46 rows=46064 width=8) (actual time=0.027..202.552 rows=36686 loops=5)
Filter: (is_active AND (NOT ucams_sync))
Rows Removed by Filter: 58555
-> Hash (cost=33440.21..33440.21 rows=419241 width=8) (actual time=672.711..672.711 rows=544383 loops=5)
Buckets: 1048576 (originally 524288) Batches: 2 (originally 1) Memory Usage: 20481kB
-> Seq Scan on core_contract (cost=0.00..33440.21 rows=419241 width=8) (actual time=0.054..421.413 rows=544383 loops=5)
Filter: ((((billing_id)::text = 'rb'::text) OR is_fake_flag) AND (isp_org_id = ANY ('{77,310,311}'::integer[])))
Rows Removed by Filter: 92184
-> Index Scan using auth_user_id_pkey on auth_user (cost=0.42..0.55 rows=1 width=4) (actual time=0.008..0.008 rows=0 loops=67045)
Index Cond: (id = core_contract.user_id)
Filter: (last_login IS NOT NULL)
Rows Removed by Filter: 1
-> Index Scan using core_erphouse_id_pkey on core_erphouse (cost=0.42..1.45 rows=1 width=4) (actual time=0.006..0.006 rows=0 loops=1219)
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.223 ms
Execution time: 1151.360 ms
(27 rows)
Внимание привлекает строчка с фрагментом Memory Usage: 20481kB
. Попытки упросить запрос или создать дополнительные индексы для исключений последовательных сканирований успехом не увенчались - планировщик упрямо игнорировал создаваемые индексы и продолжал использовать последовательный перебор. Дело в том, что для соединения строк двух таблиц в данном случае используется созданный налету хэш-массив объёмом 20 мегабайт. Этот хэш-массив располагается в буфере work_mem
, выделяемый для каждого установленного подключения. Размера этого буфера оказалось недостаточно для хранения хэш-массива, в результате чего PostgreSQL прибег к использованию временных файлов.
Ситуацию помог исправить дополнительный индекс типа HASH:
CREATE INDEX core_contract_hash_id ON core_contract USING HASH (id);
После его создания план выполнения запроса изменился следующим образом:
smart_house_prod=# EXPLAIN ANALYZE SELECT COUNT(*) AS "__count"
FROM "core_contractobject"
INNER JOIN "core_contract" ON ("core_contractobject"."contract_id" = "core_contract"."id")
INNER JOIN "auth_user" ON ("core_contract"."user_id" = "auth_user"."id")
INNER 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)
AND "core_contractobject"."ucams_sync" = false);
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=127182.62..127182.63 rows=1 width=8) (actual time=608.546..608.546 rows=1 loops=1)
-> Gather (cost=127182.20..127182.61 rows=4 width=8) (actual time=608.362..645.020 rows=5 loops=1)
Workers Planned: 4
Workers Launched: 4
-> Partial Aggregate (cost=126182.20..126182.21 rows=1 width=8) (actual time=593.453..593.454 rows=1 loops=5)
-> Nested Loop (cost=0.85..126120.81 rows=24556 width=0) (actual time=593.450..593.450 rows=0 loops=5)
-> Nested Loop (cost=0.42..90620.32 rows=24568 width=4) (actual time=11.571..591.865 rows=244 loops=5)
-> Nested Loop (cost=0.00..73942.87 rows=30338 width=8) (actual time=0.166..472.952 rows=13409 loops=5)
-> Parallel Seq Scan on core_contractobject (cost=0.00..45654.51 rows=46066 width=8) (actual time=0.027..82.091 rows=36686 loops=5)
Filter: (is_active AND (NOT ucams_sync))
Rows Removed by Filter: 58556
-> Index Scan using core_contract_hash_id on core_contract (cost=0.00..0.61 rows=1 width=8) (actual time=0.009..0.009 rows=0 loops=183428)
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: 1
-> Index Scan using auth_user_id_pkey on auth_user (cost=0.42..0.55 rows=1 width=4) (actual time=0.008..0.008 rows=0 loops=67043)
Index Cond: (id = core_contract.user_id)
Filter: (last_login IS NOT NULL)
Rows Removed by Filter: 1
-> Index Scan using core_erphouse_id_pkey on core_erphouse (cost=0.42..1.44 rows=1 width=4) (actual time=0.005..0.005 rows=0 loops=1218)
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.692 ms
Execution time: 645.172 ms
(26 rows)
И дело не столько в том, что время выполнения уменьшилось в два раза, сколько в том, что теперь для соединения таблиц не используется созданный на лету хэш-массив. В результате удалось сэкономить пространство в буфере work_mem
и переместить хэш-массив на диск, для работы с которым теперь будет использоваться гораздо более объёмный общий буфер shared_buffer
. Часто используемые страницы этого индекса будут оседать в общём буфере shared_buffer
и снижать интенсивность нагрузки на дисковую подсистему.