web-dev-qa-db-ja.com

Postgresはインデックススキャンがはるかに優れたオプションである場合にインデックスを使用しません

2つのテーブルを結合する単純なクエリがありますが、これは非常に低速です。クエリプランが大きなテーブルでseqスキャンを実行していることがわかりましたemail_activities(約1,000万行)ネストされたループを実行するインデックスを使用すると、実際には高速になると思います。

インデックスの使用を強制するためにサブクエリを使用してクエリを書き直したところ、興味深いことに気付きました。以下の2つのクエリプランを見ると、サブクエリの結果セットを43kに制限すると、クエリプランがemail_activitiesのインデックスを使用し、サブクエリの制限を44kに設定すると、クエリプランでseqスキャンが使用されることがわかります。 email_activities。 1つは他よりも明らかに効率的ですが、Postgresは気にしません。

何が原因でしょうか?セットの1つが特定のサイズより大きい場合、ハッシュ結合の使用を強制する構成がどこかにありますか?

explain analyze SELECT COUNT(DISTINCT "email_activities"."email_recipient_id") FROM "email_activities" where email_recipient_id in (select "email_recipients"."id" from email_recipients WHERE "email_recipients"."email_campaign_id" = 1607 limit 43000);
                                                                                            QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=118261.50..118261.50 rows=1 width=4) (actual time=224.556..224.556 rows=1 loops=1)
   ->  Nested Loop  (cost=3699.03..118147.99 rows=227007 width=4) (actual time=32.586..209.076 rows=40789 loops=1)
         ->  HashAggregate  (cost=3698.94..3827.94 rows=43000 width=4) (actual time=32.572..47.276 rows=43000 loops=1)
               ->  Limit  (cost=0.09..3548.44 rows=43000 width=4) (actual time=0.017..22.547 rows=43000 loops=1)
                     ->  Index Scan using index_email_recipients_on_email_campaign_id on email_recipients  (cost=0.09..5422.47 rows=65710 width=4) (actual time=0.017..19.168 rows=43000 loops=1)
                           Index Cond: (email_campaign_id = 1607)
         ->  Index Only Scan using index_email_activities_on_email_recipient_id on email_activities  (cost=0.09..2.64 rows=5 width=4) (actual time=0.003..0.003 rows=1 loops=43000)
               Index Cond: (email_recipient_id = email_recipients.id)
               Heap Fetches: 40789
 Total runtime: 224.675 ms

そして:

explain analyze SELECT COUNT(DISTINCT "email_activities"."email_recipient_id") FROM "email_activities" where email_recipient_id in (select "email_recipients"."id" from email_recipients WHERE "email_recipients"."email_campaign_id" = 1607 limit 50000);
                                                                                            QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=119306.25..119306.25 rows=1 width=4) (actual time=3050.612..3050.613 rows=1 loops=1)
   ->  Hash Semi Join  (cost=4451.08..119174.27 rows=263962 width=4) (actual time=1831.673..3038.683 rows=47935 loops=1)
         Hash Cond: (email_activities.email_recipient_id = email_recipients.id)
         ->  Seq Scan on email_activities  (cost=0.00..107490.96 rows=9359988 width=4) (actual time=0.003..751.988 rows=9360039 loops=1)
         ->  Hash  (cost=4276.08..4276.08 rows=50000 width=4) (actual time=34.058..34.058 rows=50000 loops=1)
               Buckets: 8192  Batches: 1  Memory Usage: 1758kB
               ->  Limit  (cost=0.09..4126.08 rows=50000 width=4) (actual time=0.016..27.302 rows=50000 loops=1)
                     ->  Index Scan using index_email_recipients_on_email_campaign_id on email_recipients  (cost=0.09..5422.47 rows=65710 width=4) (actual time=0.016..22.244 rows=50000 loops=1)
                           Index Cond: (email_campaign_id = 1607)
 Total runtime: 3050.660 ms
  • バージョン:gcc(Ubuntu/Linaro 4.6.3-1ubuntu5)でコンパイルされたx86_64-unknown-linux-gnu上のPostgreSQL 9.3.10 4.6.3、64ビット
  • email_activities:約1,000万行
  • email_recipients:〜1100万行
16
Ryan Her

インデックススキャン->ビットマップインデックススキャン->順次スキャン

いくつかの行では、インデックススキャンを実行するのにお金がかかります。返す行が多いほど(テーブルの割合が高くなり、データの分布、値の頻度、行の幅に応じて)、1つのデータページで複数の行が見つかる可能性が高くなります。次に、ビットマップインデックススキャンに切り替えることは有料です。いずれにせよ、データページの大部分にアクセスする必要がある場合は、順次スキャンを実行し、余剰行をフィルタリングして、インデックスのオーバーヘッドを完全にスキップする方が安上がりです。

Postgresはrows=263962を見つけることを期待して順次スキャンに切り替えます。これはすでにテーブル全体の3%です。 (実際にはrows=47935のみが見つかりますが、以下を参照してください。)

この関連回答の詳細:

クエリプランの強制に注意する

Postgresで特定のプランナーメソッドを直接強制することはできませんが、otherメソッドをデバッグ目的で非常に高価に見せることはできます。マニュアルの Planner Method Configuration を参照してください。

SET enable_seqscan = off(別の回答で提案されているように)は、順次スキャンに対してそれを行います。ただし、これはセッションでのデバッグのみを目的としています。 not何をしているのか正確に知らない限り、これを本番環境での一般的な設定として使用してください。ばかげたクエリプランを強制できます。 マニュアルの引用

これらの構成パラメーターは、クエリオプティマイザーによって選択されたクエリプランに影響を与える大まかな方法​​を提供します。特定のクエリに対してオプティマイザによって選択されたデフォルトの計画が最適でない場合、temporaryソリューションは、オプティマイザに強制的にこれらの構成パラメータの1つを使用することです別のプランを選択してください。オプティマイザが選択した計画の品質を向上させるより良い方法には、平面コスト定数の調整( セクション18.7.2 を参照)、手動で ANALYZE を実行し、 default_statistics_target 構成パラメーター。ALTER TABLE SET STATISTICSを使用して、特定の列について収集される統計量を増やします。

それはあなたが必要としているアドバイスのほとんどです。

この特定のケースでは、Postgresはemail_activities.email_recipient_idで実際に検出されるよりも5〜6倍多くのヒットを期待しています。

推定rows=227007actual ... rows=40789
推定rows=263962actual ... rows=47935

このクエリを頻繁に実行する場合は、ANALYZEでより大きなサンプルを調べて、特定の列のより正確な統計を取得するのにお金がかかります。テーブルが大きい(約1,000万行)ので、次のようにします。

ALTER TABLE email_activities ALTER COLUMN email_recipient_id
SET STATISTICS 3000;  -- max 10000, default 100

次にANALYZE email_activities;

最終手段の測定

非常にまれなケースでは、別のトランザクションまたは独自の環境を持つ関数でSET LOCAL enable_seqscan = offを使用してインデックスを強制的に使用する場合があります。お気に入り:

CREATE OR REPLACE FUNCTION f_count_dist_recipients(_email_campaign_id int, _limit int)
  RETURNS bigint AS
$func$
   SELECT COUNT(DISTINCT a.email_recipient_id)
   FROM   email_activities a
   WHERE  a.email_recipient_id IN (
      SELECT id
      FROM   email_recipients
      WHERE  email_campaign_id = $1
      LIMIT  $2)       -- or consider query below
$func$  LANGUAGE sql VOLATILE COST 100000 SET enable_seqscan = off;

この設定は、関数のローカルスコープにのみ適用されます。

警告:これは概念の証明にすぎません。これほど根本的な手作業による介入でさえ、長期的にはあなたを噛むかもしれません。カーディナリティ、値の頻度、スキーマ、Postgresのグローバル設定など、すべてが時間とともに変化します。新しいPostgresバージョンにアップグレードします。現在強制しているクエリプランは、後で非常に悪い考えになる可能性があります。

通常、これはセットアップの問題の回避策にすぎません。見つけて修正してください。

代替クエリ

質問には重要な情報が欠けていますが、この同等のクエリはおそらくより高速で、(email_recipient_id)のインデックスを使用する可能性が高くなります。LIMITが大きくなるほど、その可能性は高くなります。

SELECT COUNT(*) AS ct
FROM  (
   SELECT id
   FROM   email_recipients
   WHERE  email_campaign_id = 1607
   LIMIT  43000
   ) r
WHERE  EXISTS (
   SELECT 1
   FROM   email_activities
   WHERE  email_recipient_id = r.id);
26

索引が存在する場合でも、順次スキャンの方が効率的です。この場合、postgresはかなり間違っていると推定しているようです。このような場合、関連するすべてのテーブルにANALYZE <TABLE>が役立ちます。そうでない場合は、変数enable_seqscanをOFFに設定して、技術的に可能な限りpostgresにインデックスを使用させることができますが、シーケンシャルスキャンの方がパフォーマンスが良いときにインデックススキャンが使用されることがあります。

2
Ctx