Postgres哈希加入批次爆炸

Postgres hash join batches explosion

我们很难确定为什么 Postgres 使用过多的批处理来解析连接。

这是对有问题的执行进行解释分析的输出: https://explain.dalibo.com/plan/xNJ#plan

 Limit  (cost=20880.87..20882.91 rows=48 width=205) (actual time=10722.953..10723.358 rows=48 loops=1)
   ->  Unique  (cost=20880.87..21718.12 rows=19700 width=205) (actual time=10722.951..10723.356 rows=48 loops=1)
         ->  Sort  (cost=20880.87..20930.12 rows=19700 width=205) (actual time=10722.950..10722.990 rows=312 loops=1)
               Sort Key: titlemetadata_titlemetadata.creation_date DESC, titlemetadata_titlemetadata.id, titlemetadata_titlemetadata.title_type, titlemetadata_titlemetadata.original_title, titlemetadata_titlemetadata.alternative_ids, titlemetadata_titlemetadata.metadata,
titlemetadata_titlemetadata.is_adult, titlemetadata_titlemetadata.is_kids, titlemetadata_titlemetadata.last_modified, titlemetadata_titlemetadata.year, titlemetadata_titlemetadata.runtime, titlemetadata_titlemetadata.rating, titlemetadata_titlemetadata.video_provider, tit
lemetadata_titlemetadata.series_id_id, titlemetadata_titlemetadata.season_number, titlemetadata_titlemetadata.episode_number
               Sort Method: quicksort  Memory: 872kB
               ->  Hash Right Join  (cost=13378.20..19475.68 rows=19700 width=205) (actual time=1926.352..10709.970 rows=2909 loops=1)
                     Hash Cond: (t4.titlemetadata_id = t3.id)
                     Filter: ((hashed SubPlan 1) OR (hashed SubPlan 2))
                     Rows Removed by Filter: 63248
                     ->  Seq Scan on video_provider_offer t4  (cost=0.00..5454.90 rows=66290 width=16) (actual time=0.024..57.893 rows=66390 loops=1)
                     ->  Hash  (cost=11314.39..11314.39 rows=22996 width=221) (actual time=489.530..489.530 rows=60096 loops=1)
                           Buckets: 65536 (originally 32768)  Batches: 32768 (originally 1)  Memory Usage: 11656kB
                           ->  Hash Right Join  (cost=5380.95..11314.39 rows=22996 width=221) (actual time=130.024..225.271 rows=60096 loops=1)
                                 Hash Cond: (video_provider_offer.titlemetadata_id = titlemetadata_titlemetadata.id)
                                 ->  Seq Scan on video_provider_offer  (cost=0.00..5454.90 rows=66290 width=16) (actual time=0.011..32.950 rows=66390 loops=1)
                                 ->  Hash  (cost=5129.28..5129.28 rows=20133 width=213) (actual time=129.897..129.897 rows=55793 loops=1)
                                       Buckets: 65536 (originally 32768)  Batches: 2 (originally 1)  Memory Usage: 7877kB
                                       ->  Merge Left Join  (cost=1.72..5129.28 rows=20133 width=213) (actual time=0.041..93.057 rows=55793 loops=1)
                                             Merge Cond: (titlemetadata_titlemetadata.id = t3.series_id_id)
                                             ->  Index Scan using titlemetadata_titlemetadata_pkey on titlemetadata_titlemetadata  (cost=1.30..4130.22 rows=20133 width=205) (actual time=0.028..62.949 rows=43921 loops=1)
                                                   Filter: ((NOT is_adult) AND (NOT (hashed SubPlan 3)) AND (((title_type)::text = 'MOV'::text) OR ((title_type)::text = 'TVS'::text) OR ((title_type)::text = 'TVP'::text) OR ((title_type)::text = 'EVT'::text)))
                                                   Rows Removed by Filter: 14121
                                                   SubPlan 3
                                                     ->  Seq Scan on cable_operator_cableoperatorexcludedtitle u0_2  (cost=0.00..1.01 rows=1 width=8) (actual time=0.006..0.006 rows=0 loops=1)
                                                           Filter: (cable_operator_id = 54)
                                             ->  Index Scan using titlemetadata_titlemetadata_series_id_id_73453db4_uniq on titlemetadata_titlemetadata t3  (cost=0.41..3901.36 rows=58037 width=16) (actual time=0.011..9.375 rows=12887 loops=1)
                     SubPlan 1
                       ->  Hash Join  (cost=44.62..885.73 rows=981 width=8) (actual time=0.486..36.806 rows=5757 loops=1)
                             Hash Cond: (w2.device_id = w3.id)
                             ->  Nested Loop  (cost=43.49..866.20 rows=2289 width=16) (actual time=0.441..33.096 rows=20180 loops=1)
                                   ->  Nested Loop  (cost=43.06..414.98 rows=521 width=8) (actual time=0.426..9.952 rows=2909 loops=1)
                                         Join Filter: (w1.id = w0.video_provider_id)
                                         ->  Nested Loop  (cost=42.65..54.77 rows=13 width=24) (actual time=0.399..0.532 rows=15 loops=1)
                                               ->  HashAggregate  (cost=42.50..42.95 rows=45 width=16) (actual time=0.390..0.403 rows=45 loops=1)
                                                     Group Key: v0.id
                                                     ->  Nested Loop  (cost=13.34..42.39 rows=45 width=16) (actual time=0.095..0.364 rows=45 loops=1)
                                                           ->  Hash Semi Join  (cost=13.19..32.72 rows=45 width=8) (actual time=0.084..0.229 rows=45 loops=1)
                                                                 Hash Cond: (v1.id = u0.id)
                                                                 ->  Seq Scan on cable_operator_cableoperatorprovider v1  (cost=0.00..17.36 rows=636 width=16) (actual time=0.010..0.077 rows=636 loops=1)
                                                                 ->  Hash  (cost=12.63..12.63 rows=45 width=8) (actual time=0.046..0.046 rows=45 loops=1)
                                                                       Buckets: 1024  Batches: 1  Memory Usage: 10kB
                                                                       ->  Index Scan using cable_operator_cableoperatorprovider_4d6e54b3 on cable_operator_cableoperatorprovider u0  (cost=0.28..12.63 rows=45 width=8) (actual time=0.016..0.035 rows=45 loops=1)
                                                                             Index Cond: (cable_operator_id = 54)
                                                           ->  Index Only Scan using video_provider_videoprovider_pkey on video_provider_videoprovider v0  (cost=0.15..0.20 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=45)
                                                                 Index Cond: (id = v1.provider_id)
                                                                 Heap Fetches: 45
                                               ->  Index Scan using video_provider_videoprovider_pkey on video_provider_videoprovider w1  (cost=0.15..0.25 rows=1 width=8) (actual time=0.002..0.002 rows=0 loops=45)
                                                     Index Cond: (id = v0.id)
                                                     Filter: ((video_provider_type)::text = 'VOD'::text)
                                                     Rows Removed by Filter: 1
                                         ->  Index Scan using video_provider_offer_da942d2e on video_provider_offer w0  (cost=0.42..27.22 rows=39 width=16) (actual time=0.026..0.585 rows=194 loops=15)
                                               Index Cond: (video_provider_id = v0.id)
                                               Filter: (((end_date > '2021-09-02 19:23:00-03'::timestamp with time zone) OR (end_date IS NULL)) AND (access_criteria && '{vtv_mas,TBX_LOGIN,urn:spkg:tve:fox-premium,urn:tve:mcp,AMCHD,AMC_CONSORCIO,ANIMAL_PLANET,ASUNTOS_PUBLI
COS,ASUNTOS_PUBLICOS_CONSORCIO,CINECANALLIVE,CINECANAL_CONSORCIO,DISCOVERY,DISCOVERY_KIDS_CONSORCIO,DISCOVERY_KIDS_OD,DISNEY,DISNEY_CH_CONSORCIO,DISNEY_XD,DISNEY_XD_CONSORCIO,EL_CANAL_HD,EL_CANAL_HD_CONSORCIO,EL_GOURMET_CONSORCIO,ESPN,ESPN2_HD_CONSORCIO,ESPN3_HD_CONSORCIO
,ESPNMAS_HD_CONSORCIO,ESPN_BASIC,ESPN_HD_CONSORCIO,ESPN_PLAY,EUROPALIVE,EUROPA_EUROPA,EUROPA_EUROPA_CONSORCIO,FILMANDARTS_DISPOSITIVOS,FILMS_ARTS,FILM_AND_ARTS_CONSORCIO,FOXLIFE,FOX_LIFE_CONSORCIO,FOX_SPORTS_1_DISPOSITIVOS,FOX_SPORTS_2_DISPOSITIVOS,FOX_SPORTS_2_HD_CONSORC
IO,FOX_SPORTS_3_DISPOSITIVOS,FOX_SPORTS_3_HD_CONSORCIO,FOX_SPORTS_HD_CONSORCIO,FRANCE24_DISPOSITIVOS,FRANCE_24_CONSORCIO,GOURMET,GOURMET_DISPOSITIVOS,HOME_HEALTH,INVESTIGATION_DISCOVERY,MAS_CHIC,NATGEOKIDS_DISPOSITIVOS,NATGEO_CONSORCIO,NATGEO_DISPOSITIVOS,NATGEO_KIDS_CONS
ORCIO,PASIONES,PASIONES_CONSORCIO,SVOD_TYC_BASIC,TBX_LOGIN,TCC_2_CONSORCIO,TCC_2_HD,TLC,TVE,TVE_CONSORCIO,TYC_SPORTS_CONSORCIO,VTV_LIVE,clarosports,discoverykids,espnplay_south_alt,urn:spkg:tve:fox-basic,urn:tve:babytv,urn:tve:cinecanal,urn:tve:discoverykids,urn:tve:foxli
fe,urn:tve:fp,urn:tve:fx,urn:tve:natgeo,urn:tve:natgeokids,urn:tve:natgeowild,urn:tve:thefilmzone}'::character varying(50)[]) AND ((((content_type)::text = 'VOD'::text) AND ((start_date < '2021-09-02 19:23:00-03'::timestamp with time zone) OR (start_date IS NULL))) OR ((c
ontent_type)::text = 'LIV'::text)))
                                               Rows Removed by Filter: 5
                                   ->  Index Only Scan using video_provider_offer_devices_offer_id_device_id_key on video_provider_offer_devices w2  (cost=0.42..0.81 rows=6 width=16) (actual time=0.004..0.007 rows=7 loops=2909)
                                         Index Cond: (offer_id = w0.id)
                                         Heap Fetches: 17828
                             ->  Hash  (cost=1.10..1.10 rows=3 width=8) (actual time=0.029..0.029 rows=2 loops=1)
                                   Buckets: 1024  Batches: 1  Memory Usage: 9kB
                                   ->  Seq Scan on platform_device_device w3  (cost=0.00..1.10 rows=3 width=8) (actual time=0.024..0.027 rows=2 loops=1)
                                         Filter: ((device_code)::text = ANY ('{ANDROID,ott_dual_tcc,ott_k2_tcc}'::text[]))
                                         Rows Removed by Filter: 5
                     SubPlan 2
                       ->  Hash Join  (cost=44.62..885.73 rows=981 width=8) (actual time=0.410..33.580 rows=5757 loops=1)
                             Hash Cond: (w2_1.device_id = w3_1.id)
                             ->  Nested Loop  (cost=43.49..866.20 rows=2289 width=16) (actual time=0.375..29.886 rows=20180 loops=1)
                                   ->  Nested Loop  (cost=43.06..414.98 rows=521 width=8) (actual time=0.366..9.134 rows=2909 loops=1)
                                         Join Filter: (w1_1.id = w0_1.video_provider_id)
                                         ->  Nested Loop  (cost=42.65..54.77 rows=13 width=24) (actual time=0.343..0.476 rows=15 loops=1)
                                               ->  HashAggregate  (cost=42.50..42.95 rows=45 width=16) (actual time=0.333..0.347 rows=45 loops=1)
                                                     Group Key: v0_1.id
                                                     ->  Nested Loop  (cost=13.34..42.39 rows=45 width=16) (actual time=0.083..0.311 rows=45 loops=1)
                                                           ->  Hash Semi Join  (cost=13.19..32.72 rows=45 width=8) (actual time=0.076..0.202 rows=45 loops=1)
                                                                 Hash Cond: (v1_1.id = u0_1.id)
                                                                 ->  Seq Scan on cable_operator_cableoperatorprovider v1_1  (cost=0.00..17.36 rows=636 width=16) (actual time=0.005..0.057 rows=636 loops=1)
                                                                 ->  Hash  (cost=12.63..12.63 rows=45 width=8) (actual time=0.038..0.038 rows=45 loops=1)
                                                                       Buckets: 1024  Batches: 1  Memory Usage: 10kB
                                                                       ->  Index Scan using cable_operator_cableoperatorprovider_4d6e54b3 on cable_operator_cableoperatorprovider u0_1  (cost=0.28..12.63 rows=45 width=8) (actual time=0.007..0.020 rows=45 loops=1)
                                                                             Index Cond: (cable_operator_id = 54)
                                                           ->  Index Only Scan using video_provider_videoprovider_pkey on video_provider_videoprovider v0_1  (cost=0.15..0.20 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=45)
                                                                 Index Cond: (id = v1_1.provider_id)
                                                                 Heap Fetches: 45
                                               ->  Index Scan using video_provider_videoprovider_pkey on video_provider_videoprovider w1_1  (cost=0.15..0.25 rows=1 width=8) (actual time=0.002..0.002 rows=0 loops=45)
                                                     Index Cond: (id = v0_1.id)
                                                     Filter: ((video_provider_type)::text = 'VOD'::text)
                                                     Rows Removed by Filter: 1
                                         ->  Index Scan using video_provider_offer_da942d2e on video_provider_offer w0_1  (cost=0.42..27.22 rows=39 width=16) (actual time=0.022..0.536 rows=194 loops=15)
                                               Index Cond: (video_provider_id = v0_1.id)
                                               Filter: (((end_date > '2021-09-02 19:23:00-03'::timestamp with time zone) OR (end_date IS NULL)) AND (access_criteria && '{vtv_mas,TBX_LOGIN,urn:spkg:tve:fox-premium,urn:tve:mcp,AMCHD,AMC_CONSORCIO,ANIMAL_PLANET,ASUNTOS_PUBLI
COS,ASUNTOS_PUBLICOS_CONSORCIO,CINECANALLIVE,CINECANAL_CONSORCIO,DISCOVERY,DISCOVERY_KIDS_CONSORCIO,DISCOVERY_KIDS_OD,DISNEY,DISNEY_CH_CONSORCIO,DISNEY_XD,DISNEY_XD_CONSORCIO,EL_CANAL_HD,EL_CANAL_HD_CONSORCIO,EL_GOURMET_CONSORCIO,ESPN,ESPN2_HD_CONSORCIO,ESPN3_HD_CONSORCIO
,ESPNMAS_HD_CONSORCIO,ESPN_BASIC,ESPN_HD_CONSORCIO,ESPN_PLAY,EUROPALIVE,EUROPA_EUROPA,EUROPA_EUROPA_CONSORCIO,FILMANDARTS_DISPOSITIVOS,FILMS_ARTS,FILM_AND_ARTS_CONSORCIO,FOXLIFE,FOX_LIFE_CONSORCIO,FOX_SPORTS_1_DISPOSITIVOS,FOX_SPORTS_2_DISPOSITIVOS,FOX_SPORTS_2_HD_CONSORC
IO,FOX_SPORTS_3_DISPOSITIVOS,FOX_SPORTS_3_HD_CONSORCIO,FOX_SPORTS_HD_CONSORCIO,FRANCE24_DISPOSITIVOS,FRANCE_24_CONSORCIO,GOURMET,GOURMET_DISPOSITIVOS,HOME_HEALTH,INVESTIGATION_DISCOVERY,MAS_CHIC,NATGEOKIDS_DISPOSITIVOS,NATGEO_CONSORCIO,NATGEO_DISPOSITIVOS,NATGEO_KIDS_CONS
ORCIO,PASIONES,PASIONES_CONSORCIO,SVOD_TYC_BASIC,TBX_LOGIN,TCC_2_CONSORCIO,TCC_2_HD,TLC,TVE,TVE_CONSORCIO,TYC_SPORTS_CONSORCIO,VTV_LIVE,clarosports,discoverykids,espnplay_south_alt,urn:spkg:tve:fox-basic,urn:tve:babytv,urn:tve:cinecanal,urn:tve:discoverykids,urn:tve:foxli
fe,urn:tve:fp,urn:tve:fx,urn:tve:natgeo,urn:tve:natgeokids,urn:tve:natgeowild,urn:tve:thefilmzone}'::character varying(50)[]) AND ((((content_type)::text = 'VOD'::text) AND ((start_date < '2021-09-02 19:23:00-03'::timestamp with time zone) OR (start_date IS NULL))) OR ((c
ontent_type)::text = 'LIV'::text)))
                                               Rows Removed by Filter: 5
                                   ->  Index Only Scan using video_provider_offer_devices_offer_id_device_id_key on video_provider_offer_devices w2_1  (cost=0.42..0.81 rows=6 width=16) (actual time=0.003..0.006 rows=7 loops=2909)
                                         Index Cond: (offer_id = w0_1.id)
                                         Heap Fetches: 17828
                             ->  Hash  (cost=1.10..1.10 rows=3 width=8) (actual time=0.015..0.015 rows=2 loops=1)
                                   Buckets: 1024  Batches: 1  Memory Usage: 9kB
                                   ->  Seq Scan on platform_device_device w3_1  (cost=0.00..1.10 rows=3 width=8) (actual time=0.010..0.011 rows=2 loops=1)
                                         Filter: ((device_code)::text = ANY ('{ANDROID,ott_dual_tcc,ott_k2_tcc}'::text[]))
                                         Rows Removed by Filter: 5
 Planning time: 8.255 ms
 Execution time: 10723.830 ms
(100 rows)

奇怪的是,同一个查询,有时只使用一个批次。这是一个例子:https://explain.dalibo.com/plan/zTv#plan

这是正在使用的 work_mem:

show work_mem;
 work_mem
----------
 8388kB
(1 row)

我对更改查询以提高性能不感兴趣,但有兴趣了解不同行为的原因。

我发现这个帖子显然与此相关,但我不太明白他们在说什么:https://www.postgresql.org/message-id/flat/CA%2BhUKGKWWmf%3DWELLG%3DaUGbcugRaSQbtm0tKYiBut-B2rVKX63g%40mail.gmail.com

谁能告诉我为什么会出现这种不同的行为?两种情况下的基础数据相同。

如果哈希是在内存中完成的,那么只会有一个批次。

与原始散列批号不同的原因是 Postgres 选择增加批号以减少内存消耗。

您可能会发现这个 EXPLAIN 词汇表很有用(免责声明:我是作者之一),这里是 the page on Hash Batches,它也链接到 PostgreSQL 源代码(它用简单的英语很好地记录)。

虽然不是完美的启发式方法,但您可以看到多批次操作所需的内存大约或高于您的 work_mem 设置。它们可能比它低,因为磁盘上的操作通常需要更少的内存。

我不是 100% 确定为什么在您的确切情况下选择了一个而不是另一个,但看起来确实存在一些非常细微的行估计差异,这可能是一个很好的起点。

从 PostgreSQL 13 开始,现在还有一个 hash_mem_multiplier 设置,可用于为散列提供更多内存,而无需为其他操作(如排序)这样做。

我们只要VACUUM FULL ANALYZE;就能解决问题。 在那之后,一切开始按预期工作 (https://explain.depesz.com/s/eoqH#html)

旁注:我们不知道我们应该每天都这样做。