web-dev-qa-db-ja.com

Postgresの行レベルのセキュリティポリシーは、インラインバージョンに比べて最適化が不十分です

次のようなクエリがあります。

  SELECT post.id, post.author_id, post.published_at, post.content
    FROM post
   WHERE post.group_id = 1
ORDER BY post.published_at DESC, post.id
   LIMIT 5;

このクエリには(group_id, published_at DESC, id)のインデックスがあり、行レベルセキュリティ(RLS)ポリシーが使用されていない場合にこのクエリプランが提供されます。

 Limit  (cost=0.14..1.12 rows=5 width=143)
   ->  Index Scan using post_published_at on post  (cost=0.14..15.86 rows=80 width=143)
         Index Cond: (group_id = 1)

次に、このポリシーを追加します。

CREATE POLICY select_member_of ON post FOR SELECT USING
  (EXISTS (SELECT 1
             FROM group_member
            WHERE group_member.account_id = current_setting('current_account_id', false)::INT AND
                  group_member.group_id = post.group_id));

group_member.account_idテーブルのgroup_member.group_idgroup_memberに複合主キーがあります。

group_membergroup_member.account_idの両方が定数値に設定されるため、Postgresがこのクエリをgroup_member.group_idのインデックスのみのスキャンとして計画することを期待しています。上記のSELECTクエリのgroup_member.group_id条件のため、WHERE post.group_id = 1は定数でなければなりません。

実際、RLSポリシーを次のようなクエリにインライン化すると、これが発生しているように見えます。

  SELECT id, author_id, published_at, content
    FROM post
   WHERE group_id = 1 AND
         (EXISTS (SELECT 1
                    FROM group_member
                   WHERE group_member.account_id = current_setting('current_account_id', false)::INT AND
                         group_member.group_id = post.group_id))
ORDER BY published_at DESC, id
   LIMIT 5;

クエリプランを取得します。

 Limit  (cost=0.30..1.85 rows=5 width=143)
   ->  Nested Loop Semi Join  (cost=0.30..25.04 rows=80 width=143)
         ->  Index Scan using post_published_at on post  (cost=0.14..15.86 rows=80 width=147)
               Index Cond: (group_id = 1)
         ->  Materialize  (cost=0.16..8.19 rows=1 width=4)
               ->  Index Only Scan using group_member_pkey on group_member  (cost=0.16..8.18 rows=1 width=4)
                     Index Cond: ((account_id = (current_setting('current_account_id'::text, false))::integer) AND (group_id = 1))

それは私が探していたものです。ただし、実際のRLSポリシーを使用してクエリを実行すると、クエリプランは次のようになります。

 Limit  (cost=23.08..23.10 rows=5 width=143)
   ->  Sort  (cost=23.08..23.28 rows=80 width=143)
         Sort Key: post.published_at DESC, post.id
         ->  Subquery Scan on post  (cost=8.92..21.75 rows=80 width=143)
               ->  Nested Loop Semi Join  (cost=8.92..20.95 rows=80 width=147)
                     ->  Bitmap Heap Scan on post post_1  (cost=8.76..11.76 rows=80 width=147)
                           Recheck Cond: (group_id = 1)
                           ->  Bitmap Index Scan on post_published_at  (cost=0.00..8.74 rows=80 width=0)
                                 Index Cond: (group_id = 1)
                     ->  Materialize  (cost=0.16..8.20 rows=1 width=4)
                           ->  Subquery Scan on group_member  (cost=0.16..8.19 rows=1 width=4)
                                 ->  Index Only Scan using group_member_pkey on group_member group_member_1  (cost=0.16..8.18 rows=1 width=8)
                                       Index Cond: ((account_id = (current_setting('current_account_id'::text, false))::integer) AND (group_id = 1))

それはかなり悪いです。

これは予想される動作ですか? RLSポリシーをインライン化したバージョンと同じクエリプランを取得する方法はありますか?

4
Calebmer

サンプルデータがないと、正確なシナリオを再現することは困難です。

政策の表現はできるだけシンプルにすべきだと思います。あなたの場合、それは:

CREATE POLICY select_member_of ON post FOR SELECT USING (group_id IN (SELECT group_id
            FROM group_member
            WHERE account_id = current_setting('current_account_id', false)::INT));
1
Colin 't Hart