開発サーバー上のアプリケーションを最適化しています。データベースは時々削除されて復元され(常に同じデータベースとデータ)、キャッシュはフラッシュされます。
子テーブルに行がある親テーブルの行を識別したい。
親テーブルには約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 |
_
これらの変更を試しましたが、プランの選択に違いはありません。
VACUUM FULL ANALYZE
_REINDEX TABLE child
_ALTER TABLE child ALTER COLUMN parent_id SET STATISTICS 10000
_SELECT pg_stat_reset()
random_page_cost
_を1.0に変更(ストレージはSSD)HashAggregatesが無効になっている場合、プランナーは目的の計画を生成します。
ndistinct
を実際の一意の_parent_id
_ sの数に近いものに変更すると、プランナーがコンソールで遅いプランを生成する傾向があります。
* pgbadgerによって報告されたように、100msを超える遅いクエリがログに記録されます
最終的に私は 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)
これは効果的な回避策ですが、次の理由でやや不十分です。
*ログは33の遅いクエリを報告しましたが、これは1600からの許容可能な削減です。