SELECT DISTINCT ONサブクエリが非効率的なプランを使用しています


8

私はテーブルを持っていますprogresses(現在何十万ものレコードが含まれています):

    Column     |            Type             |                        Modifiers                        
---------------+-----------------------------+---------------------------------------------------------
 id            | integer                     | not null default nextval('progresses_id_seq'::regclass)
 lesson_id     | integer                     | 
 user_id       | integer                     | 
 created_at    | timestamp without time zone | 
 deleted_at    | timestamp without time zone | 
Indexes:
    "progresses_pkey" PRIMARY KEY, btree (id)
    "index_progresses_on_deleted_at" btree (deleted_at)
    "index_progresses_on_lesson_id" btree (lesson_id)
    "index_progresses_on_user_id" btree (user_id)

そしてビューv_latest_progresses直近を照会progressuser_idlesson_id

SELECT DISTINCT ON (progresses.user_id, progresses.lesson_id)
  progresses.id AS progress_id,
  progresses.lesson_id,
  progresses.user_id,
  progresses.created_at,
  progresses.deleted_at
 FROM progresses
WHERE progresses.deleted_at IS NULL
ORDER BY progresses.user_id, progresses.lesson_id, progresses.created_at DESC;

ユーザーは特定のレッスンで多くの進行状況を持つことができますが、多くの場合、特定のユーザーまたはレッスン(または2つの組み合わせ)のセットで最近作成された進行状況のセットを照会する必要があります。

ビューv_latest_progressesはこれを適切に実行し、一連のuser_ids を指定するとパフォーマンスが向上します。

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN ([the same list of ids given by the subquery in the second example below]);
                                                                               QUERY PLAN                                                                                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=526.68..528.66 rows=36 width=57)
   ->  Sort  (cost=526.68..527.34 rows=265 width=57)
         Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
         ->  Index Scan using index_progresses_on_user_id on progresses  (cost=0.47..516.01 rows=265 width=57)
               Index Cond: (user_id = ANY ('{ [the above list of user ids] }'::integer[]))
               Filter: (deleted_at IS NULL)
(6 rows)

ただし、user_ids のセットをサブクエリに置き換えて同じクエリを実行しようとすると、非常に非効率になります。

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Merge Semi Join  (cost=69879.08..72636.12 rows=19984 width=57)
   Merge Cond: (progresses.user_id = users.id)
   ->  Unique  (cost=69843.45..72100.80 rows=39969 width=57)
         ->  Sort  (cost=69843.45..70595.90 rows=300980 width=57)
               Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
               ->  Seq Scan on progresses  (cost=0.00..31136.31 rows=300980 width=57)
                     Filter: (deleted_at IS NULL)
   ->  Sort  (cost=35.63..35.66 rows=10 width=4)
         Sort Key: users.id
         ->  Index Scan using index_users_on_company_id on users  (cost=0.42..35.46 rows=10 width=4)
               Index Cond: (company_id = 44)
(11 rows)

私が理解しようとしているのは、PostgreSQLが2番目の例のサブクエリでフィルタリングする前にテーブルDISTINCT全体に対してクエリを実行する理由progressesです。

このクエリを改善する方法について何かアドバイスはありますか?

回答:


11

アーロン、

私の最近の仕事では、PostgreSQLでいくつかの同様の質問を調査しています。ほとんどの場合、PostgreSQLは適切なクエリプランを生成するのに優れていますが、常に完璧とは限りません。

いくつかの簡単な提案はANALYZEprogressesテーブルでを実行して統計を更新したことを確認することですが、これ問題の解決を保証するものではありません

この投稿にはおそらく長すぎると思われる理由で、の統計収集ANALYZEとクエリプランナーに、長期的に解決する必要があるかもしれない奇妙な動作がいくつか見つかりました。短期的には、必要なクエリプランを試すためにクエリを書き直すことがコツです。

テストのためにデータにアクセスできない場合は、次の2つの提案を行います。

1)使用 ARRAY()

PostgreSQLのクエリプランナーでは、配列とレコードセットの扱いが異なります。場合によっては、同じクエリプランが作成されることがあります。この場合、私の場合の多くと同様に、そうではありません。

あなたの元のクエリでは:

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" 
IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);

それを修正しようとする最初のパスとして、試してください

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44));

からINへのサブクエリの変更に注意してください=ANY(ARRAY())

2)CTEを使用する

別のトリックは、私の最初の提案が機能しない場合に、個別の最適化を強制することです。CTE内のクエリはメインクエリとは別に最適化および具体化されるため、多くの人がこのトリックを使用していることを知っています。

EXPLAIN 
WITH user_selection AS(
  SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44
)
SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "id" FROM user_selection));

基本的に、句user_selectionを使用してCTE を作成することによりWITH、サブクエリに対して個別の最適化を実行するようにPostgreSQLに要求します

SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44

そして、それらの結果を具体化します。次に、もう一度=ANY(ARRAY())式を使用して手動で計画を操作しようとします。

これらの場合、EXPLAINコストが最も低いソリューションが見つかると既に考えていたため、の結果だけを信頼することはできません。を実行しEXPLAIN (ANALYZE,BUFFERS)...て、時間とページの読み取りの観点から実際にかかるコストを確認してください。


結局のところ、最初の提案は不思議に機能します。そのクエリのコストは144.07..144.6、私が得ている70,000を大幅に下回ります。どうもありがとうございました。
アーロン

1
ハ!お役に立てて嬉しいです。私はこれらの "クエリプランハッキング"の問題に非常に苦労しています。それは科学の上に少しアートです。
クリス・

私は何年にもわたって、データベースに自分のやりたいことを実行させるために、左右のトリックを学んできました。それは本当に芸術です。よく考え抜かれた説明に本当に感謝しています!
アーロン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.