web-dev-qa-db-ja.com

クエリプランナーは、同じクエリに対してpsqlとアプリケーションで異なるプランを生成します

開発サーバー上のアプリケーションを最適化しています。データベースは時々削除されて復元され(常に同じデータベースとデータ)、キャッシュはフラッシュされます。

子テーブルに行がある親テーブルの行を識別したい。

親テーブルには約5000行、子テーブルには約180万行あります。 1795行が基準を満たしています。

_-- parent
           Column            |            Type             | Collation | Nullable |                  Default                   
-----------------------------+-----------------------------+-----------+----------+--------------------------------------------
 id                          | integer                     |           | not null | nextval('parent_id_seq'::regclass)



Indexes:
    "parent_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    TABLE "child" CONSTRAINT "child_parent_id_fkey" FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE RESTRICT


-- child
      Column       |            Type             | Collation | Nullable |                 Default                 
-------------------+-----------------------------+-----------+----------+-----------------------------------------
 id                | integer                     |           | not null | nextval('child_id_seq'::regclass)
 parent_id         | integer                     |           | not null | 
Indexes:
    "child_pkey" PRIMARY KEY, btree (id)
    "child_parent_id_index" btree (parent_id)
Foreign-key constraints:
    "child_parent_id_fkey" FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE RESTRICT
_

クエリを書いたところ、プランナーは13msの実行時間を報告しました。

_# EXPLAIN ANALYZE SELECT p.id FROM parent p WHERE EXISTS (SELECT 1 FROM child c WHERE c.parent_id = p.id);
                                                                           QUERY PLAN                                                                            
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop Semi Join  (cost=0.43..2299.35 rows=69 width=4) (actual time=0.193..13.188 rows=1795 loops=1)
   ->  Seq Scan on parent p  (cost=0.00..178.50 rows=4750 width=4) (actual time=0.008..0.715 rows=4750 loops=1)
   ->  Index Only Scan using child_parent_id_index on child c  (cost=0.43..487.99 rows=26447 width=4) (actual time=0.002..0.002 rows=0 loops=4750)
         Index Cond: (parent_id = p.id)
         Heap Fetches: 1795
 Planning Time: 1.197 ms
 Execution Time: 13.355 ms
(7 rows)
_

ORDER BY句を追加すると、同様のパフォーマンスになります(16ミリ秒)。

_# EXPLAIN ANALYZE SELECT p.id FROM parent p WHERE EXISTS (SELECT 1 FROM child c WHERE c.parent_id = p.id) ORDER BY p.id;
                                                                              QUERY PLAN                                                                               
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=2301.45..2301.63 rows=69 width=4) (actual time=15.915..15.996 rows=1795 loops=1)
   Sort Key: p.id
   Sort Method: quicksort  Memory: 133kB
   ->  Nested Loop Semi Join  (cost=0.43..2299.35 rows=69 width=4) (actual time=0.181..15.191 rows=1795 loops=1)
         ->  Seq Scan on parent p  (cost=0.00..178.50 rows=4750 width=4) (actual time=0.018..0.729 rows=4750 loops=1)
         ->  Index Only Scan using child_parent_id_index on child c  (cost=0.43..487.99 rows=26447 width=4) (actual time=0.003..0.003 rows=0 loops=4750)
               Index Cond: (parent_id = p.id)
               Heap Fetches: 1795
 Planning Time: 1.870 ms
 Execution Time: 16.161 ms
(10 rows)
_

ただし、クエリのどちらかのバージョンを実行するようにアプリケーションコードを変更すると、実行時間は1600回の実行で平均306ミリ秒になります。*、事前にpsqlでクエリを実行してプランナを「準備」しようとしても。

_auto_explain_は、アプリケーションの実行中にこの計画を記録します(私はそれが代表であると想定しています):

_LOG:  duration: 451.723 ms  plan:
    Query Text: SELECT "parent"."id" FROM "parent"
                    WHERE EXISTS (SELECT 1 FROM child
                                    WHERE "child"."parent_id" = "parent"."id")
                    ORDER BY "parent"."id"

    Sort  (cost=47844.13..47844.30 rows=69 width=4) (actual time=451.327..451.433 rows=1796 loops=1)
      Sort Key: parent.id
      Sort Method: quicksort  Memory: 133kB
      Buffers: shared hit=8518 read=24207
      ->  Nested Loop  (cost=47271.56..47842.02 rows=69 width=4) (actual time=442.385..450.911 rows=1796 loops=1)
            Buffers: shared hit=8518 read=24207
            ->  HashAggregate  (cost=47271.14..47271.83 rows=69 width=4) (actual time=442.355..442.716 rows=1796 loops=1)
                  Group Key: child.parent_id
                  Buffers: shared hit=212 read=24207
                  ->  Seq Scan on child  (cost=0.00..42700.71 rows=1828171 width=4) (actual time=0.038..186.566 rows=1817908 loops=1)
                        Buffers: shared hit=212 read=24207
            ->  Index Only Scan using parent_pkey on parent  (cost=0.42..8.26 rows=1 width=4) (actual time=0.004..0.004 rows=1 loops=1796)
                  Index Cond: (id = child.parent_id)
                  Heap Fetches: 3234
                  Buffers: shared hit=8306
_

Psqlのプランナーによって生成されたプランと実行時に生成されたプランの間にこのような違いがあるのはなぜですか? (そして、より良い計画を選択するようにpostgresをどのように説得できますか?)

_SELECT version();
PostgreSQL 11.7 on x86_64-redhat-linux-gnu, compiled by gcc (GCC) 9.2.1 20190827 (Red Hat 9.2.1-1), 64-bit
_

更新

_child.parent_id_の値は、1つの値に圧倒的に歪められます。1.7Mの値は9で、他のどの値も10000回以上発生しません。

_child.parent_id_のpg_statsは次のようになります(復元直後)。

_+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
schemaname             | public
tablename              | child
attname                | parent_id
inherited              | f
null_frac              | 0
avg_width              | 4
n_distinct             | 62
most_common_vals       | {9,7895,7891,7893,7885,7907,9042,7903,7884,7902,7892,7894,7886,7899,7887,7898,9041,49,7906,45,7901,44}
most_common_freqs      | {0.968433,0.00366667,0.00343333,0.0031,0.0028,0.00173333,0.0016,0.00143333,0.00126667,0.00123333,0.00116667,0.0011,0.000966667,0.000933333,0.0009,0.0007,0.0006,0.000533333,0.000533333,0.0005,0.0004,0.0003}
histogram_bounds       | {5,5,5,8,20,42,42,42,43,46,47,47,48,48,48,48,3680,3975,4118,4367,4902,5236,5332,5793,6142,6421,6980,7272,8863,9006,9006,9006,9007,9007,9010,9010,9010,9010,9014,9035}
correlation            | 0.929476
most_common_elems      | 
most_common_elem_freqs | 
elem_count_histogram   | 
_

これらの変更を試しましたが、プランの選択に違いはありません。

  • postgres、template0、template1を除く他のすべてのデータベースをドロップします
  • 増加RAMバッファ用
  • 両方のテーブルの_VACUUM FULL ANALYZE_
  • _REINDEX TABLE child_
  • _ALTER TABLE child ALTER COLUMN parent_id SET STATISTICS 10000_
  • SELECT pg_stat_reset()
  • redorder WHEREオペランド
  • _random_page_cost_を1.0に変更(ストレージはSSD)

HashAggregatesが無効になっている場合、プランナーは目的の計画を生成します。

ndistinctを実際の一意の_parent_id_ sの数に近いものに変更すると、プランナーがコンソールで遅いプランを生成する傾向があります

* pgbadgerによって報告されたように、100msを超える遅いクエリがログに記録されます

2
snakecharmerb

最終的に私は StackOverflowに関するこのQ&A を見つけ、OFFSET = 0をサブクエリに追加すると、クエリプランナーがサブクエリをインライン化できなくなります。この変更を適用すると、計画担当者は一貫して* psqlとアプリケーションで効率的なクエリを生成します。

EXPLAIN (ANALYZE, BUFFERS) SELECT "parent"."id" FROM "parent"
                        WHERE EXISTS
                         (SELECT 1 FROM child
                            WHERE "child"."parent_id" = "parent"."id" OFFSET 0);
                                                                            QUERY PLAN                                                                            
------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on parent  (cost=0.00..2739.35 rows=2375 width=4) (actual time=0.185..16.813 rows=1795 loops=1)
   Filter: (SubPlan 1)
   Rows Removed by Filter: 2955
   Buffers: shared hit=16342
   SubPlan 1
     ->  Index Only Scan using child_parent_id_index on child  (cost=0.43..3533.66 rows=31652 width=4) (actual time=0.003..0.003 rows=0 loops=4750)
           Index Cond: (parent_id = parent.id)
           Heap Fetches: 2025
           Buffers: shared hit=16211
 Planning Time: 0.328 ms
 Execution Time: 16.988 ms
(11 rows)

これは効果的な回避策ですが、次の理由でやや不十分です。

  • 計画者がより効率的な計画を選択できないようにします(存在する場合)
  • プランナーがpsqlとアプリケーションで異なるプランを生成した理由はまだわかりません

*ログは33の遅いクエリを報告しましたが、これは1600からの許容可能な削減です。

1
snakecharmerb