web-dev-qa-db-ja.com

PostgresQL再帰クエリは、低負荷でデータがほとんどないために実行速度が遅い

私は単純なACLシステムを構築しており、データストアはPostgres 9.6です。アクセス許可はサブグループを持つことができるグループに割り当てられ、ユーザーはグループに割り当てられます。

ユーザーに関連するすべての権限の取得を担当するパフォーマンスの低いクエリがあり、それを最適化/書き換えするためにあなたの助けが必要です。

スロークエリに含まれる簡略化された(ただし関連する)データモデルは次のとおりです。

CREATE TABLE IF NOT EXISTS "acl_group" (id BIGSERIAL PRIMARY KEY, parent_id BIGINT, name TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "acl_group_membership" (group_id BIGINT NOT NULL, subject_id TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "acl" (id BIGSERIAL PRIMARY KEY, group_id BIGINT NOT NULL, service TEXT NOT NULL, action TEXT NOT NULL, resources TEXT NOT NULL);

外部キーと一意の制約に加えて、2つの追加のインデックスがあります。

CREATE INDEX ON "acl_group_membership" (subject_id);
CREATE INDEX ON "acl" (group_id);

クエリ:

WITH RECURSIVE group_tree AS (
SELECT acl_group.id, acl_group.parent_id, 1 AS level
FROM acl_group
INNER JOIN acl_group_membership AS agm ON agm.group_id = acl_group.id
WHERE agm.subject_id = $1
UNION ALL
SELECT c.id, c.parent_id, p.level + 1
FROM acl_group c
INNER JOIN group_tree p on c.id = p.parent_id
)
SELECT acl.*
FROM acl
INNER JOIN group_tree on group_tree.id = acl.group_id
ORDER BY group_tree.level asc, acl.id asc;

私の非常に中程度のストレステスト(平均50リクエスト/秒)中に、Linux Dockerコンテナーで実行されているPostgresをローカルホストで実行し、DBでこのクエリのみを実行して(書き込みなし)、DB内のデータが非常に少ない(未満)すべてのテーブル間で100行)、次の結果が得られます。

  1. リクエストの約10%が非常に遅く実行され、約200ミリ秒以上でクロッキング
  2. 約60msでより多くのクロック
  3. 10ミリ秒未満でクロックが50%だけ。

高速実行クエリプラン( https://explain.depesz.com/s/M5tO ):

Sort  (cost=355.67..356.08 rows=161 width=50) (actual time=0.988..1.065 rows=26 loops=1)
  Sort Key: group_tree.level, acl.id
  Sort Method: quicksort  Memory: 27kB
  CTE group_tree
    ->  Recursive Union  (cost=13.74..324.76 rows=606 width=20) (actual time=0.110..0.412 rows=3 loops=1)
          ->  Hash Join  (cost=13.74..38.51 rows=6 width=20) (actual time=0.103..0.138 rows=1 loops=1)
                Hash Cond: (acl_group.id = agm.group_id)
                ->  Seq Scan on acl_group  (cost=0.00..20.70 rows=1070 width=16) (actual time=0.007..0.040 rows=11 loops=1)
                ->  Hash  (cost=13.67..13.67 rows=6 width=8) (actual time=0.043..0.043 rows=1 loops=1)
                      Buckets: 1024  Batches: 1  Memory Usage: 9kB
                      ->  Bitmap Heap Scan on acl_group_membership agm  (cost=4.20..13.67 rows=6 width=8) (actual time=0.021..0.024 rows=1 loops=1)
                            Recheck Cond: (subject_id = 'team_df'::text)
                            Heap Blocks: exact=1
                            ->  Bitmap Index Scan on acl_group_membership_subject_id_idx  (cost=0.00..4.20 rows=6 width=0) (actual time=0.011..0.011 rows=1 loops=1)
                                  Index Cond: (subject_id = 'team_df'::text)
          ->  Hash Join  (cost=1.95..27.41 rows=60 width=20) (actual time=0.036..0.078 rows=1 loops=3)
                Hash Cond: (c.id = p.parent_id)
                ->  Seq Scan on acl_group c  (cost=0.00..20.70 rows=1070 width=16) (actual time=0.005..0.038 rows=11 loops=2)
                ->  Hash  (cost=1.20..1.20 rows=60 width=12) (actual time=0.017..0.017 rows=1 loops=3)
                      Buckets: 1024  Batches: 1  Memory Usage: 8kB
                      ->  WorkTable Scan on group_tree p  (cost=0.00..1.20 rows=60 width=12) (actual time=0.003..0.006 rows=1 loops=3)
  ->  Hash Join  (cost=2.19..25.01 rows=161 width=50) (actual time=0.469..0.881 rows=26 loops=1)
        Hash Cond: (group_tree.id = acl.group_id)
        ->  CTE Scan on group_tree  (cost=0.00..12.12 rows=606 width=12) (actual time=0.118..0.440 rows=3 loops=1)
        ->  Hash  (cost=1.53..1.53 rows=53 width=46) (actual time=0.333..0.333 rows=53 loops=1)
              Buckets: 1024  Batches: 1  Memory Usage: 13kB
              ->  Seq Scan on acl  (cost=0.00..1.53 rows=53 width=46) (actual time=0.004..0.158 rows=53 loops=1)
Planning time: 0.454 ms
Execution time: 1.216 ms

スロークエリプラン(182ms) https://explain.depesz.com/s/pZ4e

Sort  (cost=355.67..356.08 rows=161 width=50) (actual time=92.671..182.171 rows=26 loops=1)
      Sort Key: group_tree.level, acl.id
      Sort Method: quicksort  Memory: 27kB
      CTE group_tree
        ->  Recursive Union  (cost=13.74..324.76 rows=606 width=20) (actual time=0.225..0.491 rows=3 loops=1)
              ->  Hash Join  (cost=13.74..38.51 rows=6 width=20) (actual time=0.216..0.252 rows=1 loops=1)
                    Hash Cond: (acl_group.id = agm.group_id)
                    ->  Seq Scan on acl_group  (cost=0.00..20.70 rows=1070 width=16) (actual time=0.030..0.092 rows=11 loops=1)
                    ->  Hash  (cost=13.67..13.67 rows=6 width=8) (actual time=0.094..0.094 rows=1 loops=1)
                          Buckets: 1024  Batches: 1  Memory Usage: 9kB
                          ->  Bitmap Heap Scan on acl_group_membership agm  (cost=4.20..13.67 rows=6 width=8) (actual time=0.061..0.066 rows=1 loops=1)
                                Recheck Cond: (subject_id = 'team_df'::text)
                                Heap Blocks: exact=1
                                ->  Bitmap Index Scan on acl_group_membership_subject_id_idx  (cost=0.00..4.20 rows=6 width=0) (actual time=0.016..0.016 rows=1 loops=1)
                                      Index Cond: (subject_id = 'team_df'::text)
              ->  Hash Join  (cost=1.95..27.41 rows=60 width=20) (actual time=0.031..0.067 rows=1 loops=3)
                    Hash Cond: (c.id = p.parent_id)
                    ->  Seq Scan on acl_group c  (cost=0.00..20.70 rows=1070 width=16) (actual time=0.005..0.032 rows=11 loops=2)
                    ->  Hash  (cost=1.20..1.20 rows=60 width=12) (actual time=0.014..0.014 rows=1 loops=3)
                          Buckets: 1024  Batches: 1  Memory Usage: 8kB
                          ->  WorkTable Scan on group_tree p  (cost=0.00..1.20 rows=60 width=12) (actual time=0.003..0.006 rows=1 loops=3)
      ->  Hash Join  (cost=2.19..25.01 rows=161 width=50) (actual time=92.140..92.506 rows=26 loops=1)
            Hash Cond: (group_tree.id = acl.group_id)
            ->  CTE Scan on group_tree  (cost=0.00..12.12 rows=606 width=12) (actual time=0.326..0.610 rows=3 loops=1)
            ->  Hash  (cost=1.53..1.53 rows=53 width=46) (actual time=91.781..91.781 rows=53 loops=1)
                  Buckets: 1024  Batches: 1  Memory Usage: 13kB
                  ->  Seq Scan on acl  (cost=0.00..1.53 rows=53 width=46) (actual time=0.012..10.226 rows=53 loops=1)

私はPostgresの世界に不慣れですが、そのような単純なクエリでは、データがほとんどなく、読み取り操作のみを実行すると、この速度で実行できなくなるとは思わないため、目に見えないエラーが発生すると思います。
このクエリを最適化/書き換える方法はありますか?

これは物理的なMacbook Proで実行されるLinux Dockerですが、Amazon RDSインスタンスでも同様の結果が得られます。

4
s0nica

主な問題は dezsoがコメントした のような舞台裏でサーバー上で起こっている別のことかもしれません。しかし、いくつかの観察:

あなたのクエリは私によく見えます。これは少し単純で、パフォーマンスが向上する可能性があります。わかりません、テストしてください:

WITH RECURSIVE group_tree AS (
   SELECT group_id, 1 AS level
   FROM   acl_group_membership
   WHERE  subject_id = $1

   UNION  ALL
   SELECT ag.parent_id, gt.level + 1
   FROM   group_tree gt
   JOIN   acl_group  ag USING (group_id)
)
SELECT acl.*
FROM   group_tree gt
JOIN   acl USING (group_id)
ORDER  BY gt.level, acl.id;

読み取りパフォーマンスをさらに最適化する必要があり、書き込みが少ない場合(読み取り専用についてのみ言及)、 materialized view オプションである可能性があります。

すべてのテーブル間で100行未満しかない場合、インデックスまたはビットマップインデックススキャンは表示されません。シーケンシャルスキャンのみが表示されます。一部の構成設定が正しくない(主な疑い:コスト設定)か、テーブル統計が誤解を招くかのいずれかです。

サーバー構成や自動バキューム設定を修正し、テーブルに大量の行(数千?

CREATE INDEX ON acl_group_membership (subject_id, group_id);
CREATE INDEX ON acl_group            (id, parent_id);
CREATE INDEX ON acl                  (group_id);

index-only scans を目指してacl_group_membershipおよびacl_group

私の発言 "数千?数百万?"は、スキーマのbigint列から発想を得ています。通常、プレーンなintegerは十分な大きさで、より小さくて高速です。小さくて高速なインデックスも。

仮に、毎秒新しい行を作成する場合、プレーンintegerint4)列。
2 ^ 31 /(3600 * 24 * 365)= 68.1

あなたのデザインにはほぼ同じくらいの数の書き込みがあります。

1