web-dev-qa-db-ja.com

単純なSELECTクエリの遅いパフォーマンスの最適化

私は「リンク」と呼ばれるアプリを持っています。1)ユーザーはグループに集まり、他のユーザーを追加します。2)上記のグループでお互いにコンテンツを投稿します。グループは、postgresql 9.6.5 DBの_links_group_テーブルで定義されていますが、グループに投稿される返信は_links_reply_テーブルで定義されています。全体的に、DBのパフォーマンスは優れています。

ただし、_links_reply_テーブルに対する1つのSELECTクエリが、slow_logに常に表示されます。 500ミリ秒以上かかり、他のほとんどのpostgresql操作で発生しているものよりも約10倍遅くなります。

Django ORMを使用してクエリを生成しました。ORM呼び出しは次のとおりです:replies = Reply.objects.select_related('writer__userprofile').filter(which_group=group).order_by('-submitted_on')[:25]。基本的に、これは特定のグループオブジェクトの最新の25件の返信を選択しています。また、関連するuserおよびuserprofileオブジェクトも同様です。

これは私の遅いログからの対応するSQLの例です:LOG:duration:8476.309 ms statement:

_SELECT

    "links_reply"."id",             "links_reply"."text", 
    "links_reply"."which_group_id", "links_reply"."writer_id",
    "links_reply"."submitted_on",   "links_reply"."image",
    "links_reply"."device",         "links_reply"."category", 

    "auth_user"."id",               "auth_user"."username", 

    "links_userprofile"."id",       "links_userprofile"."user_id",
    "links_userprofile"."score",    "links_userprofile"."avatar" 

FROM 

    "links_reply" 
    INNER JOIN "auth_user" 
        ON ("links_reply"."writer_id" = "auth_user"."id") 
    LEFT OUTER JOIN "links_userprofile" 
        ON ("auth_user"."id" = "links_userprofile"."user_id") 
WHERE "links_reply"."which_group_id" = 124479 
ORDER BY "links_reply"."submitted_on" DESC 
LIMIT 25
_

ここで説明の分析結果を見てください:https://explain.depesz.com/s/G4Xインデックススキャン(後方)いつも食べているようです。

これが_\d links_reply_の出力です。

_Table "public.links_reply"
     Column     |           Type           |                        Modifiers                         
----------------+--------------------------+----------------------------------------------------------
 id             | integer                  | not null default nextval('links_reply_id_seq'::regclass)
 text           | text                     | not null
 which_group_id | integer                  | not null
 writer_id      | integer                  | not null
 submitted_on   | timestamp with time zone | not null
 image          | character varying(100)   | 
 category       | character varying(15)    | not null
 device         | character varying(10)    | default '1'::character varying
Indexes:
    "links_reply_pkey" PRIMARY KEY, btree (id)
    "category_index" btree (category)
    "links_reply_submitted_on" btree (submitted_on)
    "links_reply_which_group_id" btree (which_group_id)
    "links_reply_writer_id" btree (writer_id)
    "text_index" btree (text)
Foreign-key constraints:
    "links_reply_which_group_id_fkey" FOREIGN KEY (which_group_id) REFERENCES links_group(id) DEFERRABLE INITIALLY DEFERRED
    "links_reply_writer_id_fkey" FOREIGN KEY (writer_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED
Referenced by:
    TABLE "links_groupseen" CONSTRAINT "links_groupseen_which_reply_id_fkey" FOREIGN KEY (which_reply_id) REFERENCES links_reply(id) DEFERRABLE INITIALLY DEFERRED
    TABLE "links_report" CONSTRAINT "links_report_which_reply_id_fkey" FOREIGN KEY (which_reply_id) REFERENCES links_reply(id) DEFERRABLE INITIALLY DEFERRED
_

これは大きなテーブル(2500万行)です。それが動作しているハードウェアは、16コアと60 GBのメモリを備えています。このマシンをpythonアプリと共有しています。しかし、私はサーバーのパフォーマンスを監視しており、そこにボトルネックはありません。

このクエリのパフォーマンスを向上させる方法はありますか?ここにあるすべてのオプション(ある場合)についてアドバイスしてください。


このクエリは例外的に先週までうまくを実行したことに注意してください。それから何が変わったのですか? (別のVMで)DBの_pg_dump_、次に_pg_restore_を実行し、Postgresql 9.3.10から9.6.5にアップグレードしました。また、以前にpgbouncerという接続プーラーを使用しましたが、これは新しいVMにまだ移行していません。これで完了です。

最後に、(ユーザーエクスペリエンスから)先週までに作成されたすべてのグループオブジェクトについて、クエリの実行が高速であることにも気付きました。しかし、現在作成されているすべての新しいオブジェクトは遅いログを生成しています。これは、特に_links_reply_submitted_on_インデックスを使用した場合のインデックスの問題のようなものですか?


更新:規定された最適化により、状況は一変しました。見てください:

enter image description here

3
Hassan Baig

主な問題の疑い(概要)

  1. _pg_upgrade_でメジャーバージョンをアップグレードした後、ANALYZEを実行する必要があります。テーブル統計はコピーされません。おそらくautovacuum設定も調整してください。

  2. _(which_group_id, submitted_on DESC)_の複数列インデックスは、このクエリmuchを提供する必要があります。

クエリ

ノイズをなくし、読みやすくするためのテーブルエイリアスを使用したフォーマット済みクエリ:

_SELECT lr.id, lr.text, lr.which_group_id, lr.writer_id
     , lr.submitted_on, lr.image, lr.device, lr.category
     , au.id, au.username
     , lu.id, lu.user_id, lu.score, lu.avatar
FROM   links_reply            lr
JOIN   auth_user              au ON au.id = lr.writer_id
LEFT   JOIN links_userprofile lu ON lu.user_id = au.id
WHERE  lr.which_group_id = 119287
ORDER  BY lr.submitted_on DESC
LIMIT  25;
_

クエリ自体に問題はありません。

インデックスの破損?

(私はそうは思いません。)

これはインデックス作成の問題のようなものですか?

破損が疑われる場合は、REINDEXを実行します。 マニュアルは次のことを示唆しています:

ユーザーテーブルのインデックスの破損が疑われる場合は、_REINDEX INDEX_または_REINDEX TABLE_を使用して、そのインデックスまたはテーブルのすべてのインデックスを再構築できます。

同時アクセスの場合:ロックは、いくつかの点で、インデックスを最初から削除および再作成することとは異なります。 マニュアル:

REINDEXは、インデックスの内容が最初から再構築されるという点で、インデックスのドロップと再作成に似ています。ただし、ロックに関する考慮事項はかなり異なります。 REINDEXは、インデックスの親テーブルの書き込みをロックアウトしますが、読み取りはロックアウトしません。また、処理中の特定のインデックスを排他的にロックし、そのインデックスを使用しようとする読み取りをブロックします。対照的に、_DROP INDEX_は一時的に親テーブルを排他的にロックし、書き込みと読み取りの両方をブロックします。後続の_CREATE INDEX_は、書き込みをロックアウトしますが、読み取りはロックしません。インデックスが存在しないため、読み取りはそれを使用しようとしません。つまり、ブロックはありませんが、読み取りは高価な順次スキャンに強制される可能性があります。

それでも並行操作の問題が解決しない場合は、 _CREATE INDEX CONCURRENTLY_ を検討して新しい重複インデックスを作成し、別のトランザクションで古いインデックスを削除してください。

テーブル統計

ただし、テーブル統計が実際の問題であるように見えます。クエリプランからの引用:

Links_reply 
のlinks_reply_submitted_onを使用した後方スキャンインデックス(コスト= 0.44..1,664,030.07 行= 2,001 幅= 50)
(実際の時間= 522.811..716.414 rows = 25 loops = 1)
フィルター:(which_group_id = 119287)
 フィルターによって削除された行:1721320

大胆な強調鉱山。 Postgresはこのクエリプランを誤解を招く統計に基づいているように見えます。さらに多くのヒットが予想され、おそらく述語_which_group_id = 119287_の選択性も過小評価されます。最終的に1.7M行をフィルタリングします。これreeksの不正確なテーブル統計。そして、ありそうな説明もあります:

メジャーバージョンをアップグレードする場合 _pg_upgrade_ は、既存の統計を新しいバージョンのDBにコピーしません。 _VACUUM ANALYZE_の後に_pg_upgrade_または少なくともANALYZEを実行することをお勧めします。ツールはあなたに思い出させるように促しさえします。 マニュアル:

オプティマイザ統計は_pg_upgrade_によって転送されないため、アップグレードの最後にその情報を再生成するコマンドを実行するように指示されます。新しいクラスターに一致するように接続パラメーターを設定する必要がある場合があります。

そうしないと、テーブルへの十分な書き込み(または_CREATE INDEX_や_ALTER TABLE_などの他のユーティリティコマンドが統計情報をその場で更新する)によって自動バキュームがトリガーされるまで、テーブルは現在の統計情報なしで移動します。

同じことがダンプ/復元サイクルにも当てはまります(あなたのケースでは_pg_dump_&_pg_restore_を使用)。テーブル統計はダンプに含まれません。

テーブルが非常に大きい_(~25M rows)_。 autovacuumのデフォルト設定では、row_countと固定オフセットの割合としてしきい値を定義します。大きなテーブルではこれがうまく機能しない場合があります。次の自動分析までにかなり時間がかかります。

テーブルまたはDB全体で手動ANALYZEを実行します。

関連:

より良いインデックス

...インデックスの問題、特に_links_reply_submitted_on_インデックスの場合?

はい、それも。インデックス"links_reply_submitted_on" btree (submitted_on)は、クエリ内のパターンに対して最適化されていません:

_SELECT ...
FROM   links_reply            lr
JOIN   ...
WHERE  lr.which_group_id = 119287
ORDER  BY lr.submitted_on DESC
LIMIT  25
_

上記のクエリプランで見たように、Postgresはインデックススキャンを使用して、下からインデックスを読み取り、一致しないものをフィルタリングします。このアプローチは、選択されたすべての(数少ない)_which_group_id_が最近25行ある場合、かなり高速になる可能性があります。しかし、_which_group_id_の行の不均一な分布や多くの異なる値に対しては、それほどうまく機能しません。

この複数列のインデックスの方が適しています。

_links_reply__which_group_id__submitted_on btree (which_group_id, submitted_on DESC)
_

これで、Postgresは、データの分布に関係なく、選択した_which_group_id_の上位25行を選択することができます。

関連:

詳細な説明

あなたの観察について:

最後に、(ユーザーエクスペリエンスから)先週までに作成されたすべてのグループオブジェクトについて、クエリの実行が高速であることにも気付きました。しかし、現在作成されているすべての新しいオブジェクトは遅いログを生成しています。

どうして?新しいオブジェクトはまだhave 25エントリではない可能性があるため、Postgresはさらに多くを見つけるために大きなインデックス全体をスキャンし続ける必要があります。これは、古いインデックスとクエリプランでは非常にコストがかかりますが、新しいインデックス(および更新されたテーブル統計)では非常に安価です。

また、正確なテーブル統計があれば、Postgresはおそらく他のインデックス"links_reply_which_group_id" btree (which_group_id)を使用して、既存の数行をすばやく取得します(25を超える場合はソートします)。しかし、私の新しいインデックスは、いずれにしてもより信頼できるクエリプランを提供します。

ささいなこと

テーブルレイアウトの最適化や自動バキューム設定の調整など、他にもさまざまな(マイナーな)ことができますが、この答えはすでに十分長いです。関連:

そして、あなたは後でコメントしました:

無関係なフィールドも削除しました...

実際に必要な列のみを取得することは確かに役立ちます。さらに、それを行います。しかし、ここでは主な問題ではありません。

4