Postgresはインデックススキャンではなく順次スキャンを実行しています


9

約1,000万行のテーブルと日付フィールドのインデックスがあります。結果セットに26項目しかない場合でも、インデックス付きフィールドの一意の値を抽出しようとすると、Postgresは順次スキャンを実行します。オプティマイザがこの計画を選ぶのはなぜですか?そして、私はそれを避けることができますか?

他の答えから、これはインデックスと同じくらいクエリに関連していると思います。

explain select "labelDate" from pages group by "labelDate";
                              QUERY PLAN
-----------------------------------------------------------------------
 HashAggregate  (cost=524616.78..524617.04 rows=26 width=4)
   Group Key: "labelDate"
   ->  Seq Scan on pages  (cost=0.00..499082.42 rows=10213742 width=4)
(3 rows)

テーブル構造:

http=# \d pages
                                       Table "public.pages"
     Column      |          Type          |        Modifiers
-----------------+------------------------+----------------------------------
 pageid          | integer                | not null default nextval('...
 createDate      | integer                | not null
 archive         | character varying(16)  | not null
 label           | character varying(32)  | not null
 wptid           | character varying(64)  | not null
 wptrun          | integer                | not null
 url             | text                   |
 urlShort        | character varying(255) |
 startedDateTime | integer                |
 renderStart     | integer                |
 onContentLoaded | integer                |
 onLoad          | integer                |
 PageSpeed       | integer                |
 rank            | integer                |
 reqTotal        | integer                | not null
 reqHTML         | integer                | not null
 reqJS           | integer                | not null
 reqCSS          | integer                | not null
 reqImg          | integer                | not null
 reqFlash        | integer                | not null
 reqJSON         | integer                | not null
 reqOther        | integer                | not null
 bytesTotal      | integer                | not null
 bytesHTML       | integer                | not null
 bytesJS         | integer                | not null
 bytesCSS        | integer                | not null
 bytesHTML       | integer                | not null
 bytesJS         | integer                | not null
 bytesCSS        | integer                | not null
 bytesImg        | integer                | not null
 bytesFlash      | integer                | not null
 bytesJSON       | integer                | not null
 bytesOther      | integer                | not null
 numDomains      | integer                | not null
 labelDate       | date                   |
 TTFB            | integer                |
 reqGIF          | smallint               | not null
 reqJPG          | smallint               | not null
 reqPNG          | smallint               | not null
 reqFont         | smallint               | not null
 bytesGIF        | integer                | not null
 bytesJPG        | integer                | not null
 bytesPNG        | integer                | not null
 bytesFont       | integer                | not null
 maxageMore      | smallint               | not null
 maxage365       | smallint               | not null
 maxage30        | smallint               | not null
 maxage1         | smallint               | not null
 maxage0         | smallint               | not null
 maxageNull      | smallint               | not null
 numDomElements  | integer                | not null
 numCompressed   | smallint               | not null
 numHTTPS        | smallint               | not null
 numGlibs        | smallint               | not null
 numErrors       | smallint               | not null
 numRedirects    | smallint               | not null
 maxDomainReqs   | smallint               | not null
 bytesHTMLDoc    | integer                | not null
 maxage365       | smallint               | not null
 maxage30        | smallint               | not null
 maxage1         | smallint               | not null
 maxage0         | smallint               | not null
 maxageNull      | smallint               | not null
 numDomElements  | integer                | not null
 numCompressed   | smallint               | not null
 numHTTPS        | smallint               | not null
 numGlibs        | smallint               | not null
 numErrors       | smallint               | not null
 numRedirects    | smallint               | not null
 maxDomainReqs   | smallint               | not null
 bytesHTMLDoc    | integer                | not null
 fullyLoaded     | integer                |
 cdn             | character varying(64)  |
 SpeedIndex      | integer                |
 visualComplete  | integer                |
 gzipTotal       | integer                | not null
 gzipSavings     | integer                | not null
 siteid          | numeric                |
Indexes:
    "pages_pkey" PRIMARY KEY, btree (pageid)
    "pages_date_url" UNIQUE CONSTRAINT, btree ("urlShort", "labelDate")
    "idx_pages_cdn" btree (cdn)
    "idx_pages_labeldate" btree ("labelDate") CLUSTER
    "idx_pages_urlshort" btree ("urlShort")
Triggers:
    pages_label_date BEFORE INSERT OR UPDATE ON pages
      FOR EACH ROW EXECUTE PROCEDURE fix_label_date()

回答:


8

これはPostgresの最適化に関する既知の問題です。明確な値が少ない場合-あなたの場合のように-8.4以降のバージョンを使用している場合、再帰クエリを使用した非常に高速な回避策をここで説明します:Loose Indexscan

クエリを書き直すことができます(LATERAL9.3以降のバージョンが必要)。

WITH RECURSIVE pa AS 
( ( SELECT labelDate FROM pages ORDER BY labelDate LIMIT 1 ) 
  UNION ALL
    SELECT n.labelDate 
    FROM pa AS p
         , LATERAL 
              ( SELECT labelDate 
                FROM pages 
                WHERE labelDate > p.labelDate 
                ORDER BY labelDate 
                LIMIT 1
              ) AS n
) 
SELECT labelDate 
FROM pa ;

Erwin Brandstetterは、この回答の完全な説明とクエリのいくつかのバリエーションを持っています(関連するが異なる問題について):GROUP BYクエリを最適化して、ユーザーごとに最新のレコードを取得します


6

最良のクエリは、データの分散に大きく依存します

あなたは確立された日付ごとに多くの行を持ってます。あなたのケースは結果の26の値のみに焼き付きますので、以下のソリューションはすべて、インデックスが使用されるとすぐに非常に高速になります。
(より明確な値の場合、ケースはより興味深いものになります。)

関与する必要はありませんpageid すべてで(あなたがコメントしたように)。

索引

必要なのは、の単純なbtreeインデックスだけ"labelDate"です。
列にNULL値がいくつかある場合、部分インデックスはさらに役立ちます(そして小さくなります)。

CREATE INDEX pages_labeldate_nonull_idx ON big ("labelDate")
WHERE  "labelDate" IS NOT NULL;

あなたは後で明らかにしました:

0%NULL。ただし、インポート時に修正した後のみ。

部分インデックス、NULL値を持つ行の中間状態を除外することに意味がある場合があります。インデックスへの不必要な更新を回避します(結果として膨張する)。

クエリ

暫定範囲に基づく

日付があまり多くのギャップのない連続した範囲で表示される場合は、データ型の性質を利用することができますdate。与えられた2つの値の間には、有限で数えられる数の値しかありません。ギャップが少ない場合、これが最も速くなります。

SELECT d."labelDate"
FROM  (
   SELECT generate_series(min("labelDate")::timestamp
                        , max("labelDate")::timestamp
                        , interval '1 day')::date AS "labelDate"
   FROM   pages
   ) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");

なぜへのキャストtimestampgenerate_series()?見る:

インデックスから最小値と最大値を安価に選択できます。可能な最小日付および/または最大日付を知っている場合、それはまだ少し安くなります。例:

SELECT d."labelDate"
FROM  (SELECT date '2011-01-01' + g AS "labelDate"
       FROM   generate_series(0, now()::date - date '2011-01-01' - 1) g) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");

または、不変の間隔の場合:

SELECT d."labelDate"
FROM  (SELECT date '2011-01-01' + g AS "labelDate"
       FROM generate_series(0, 363) g) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");

ルーズインデックススキャン

これは、日付の分布に非常によく機能します(日付ごとに多くの行がある場合)。基本的に、@ ypercubeがすでに提供しているもの。ただし、細かい点がいくつかあり、お気に入りのインデックスをどこでも使用できるようにする必要があります。

WITH RECURSIVE p AS (
   ( -- parentheses required for LIMIT
   SELECT "labelDate"
   FROM   pages
   WHERE  "labelDate" IS NOT NULL
   ORDER  BY "labelDate"
   LIMIT  1
   ) 
   UNION ALL
   SELECT (SELECT "labelDate" 
           FROM   pages 
           WHERE  "labelDate" > p."labelDate" 
           ORDER  BY "labelDate" 
           LIMIT  1)
   FROM   p
   WHERE  "labelDate" IS NOT NULL
   ) 
SELECT "labelDate" 
FROM   p
WHERE  "labelDate" IS NOT NULL;
  • 最初のCTE pは実質的に同じです

    SELECT min("labelDate") FROM pages

    ただし、詳細形式では、部分インデックスが使用されます。さらに、このフォームは通常、私の経験(およびテスト)で少し高速です。

  • 単一の列の場合のみ、rCTEの再帰的な項の相関サブクエリは少し高速になるはずです。これは、「labelDate」に対してNULLになる行を除外する必要があります。見る:

  • GROUP BYクエリを最適化して、ユーザーごとに最新のレコードを取得する

アサイド

引用符で囲まれていない合法的な小文字の識別子は、あなたの人生を楽にします。
ディスク領域を節約するために、テーブル定義の列を優先的に並べます。


-2

postgresqlのドキュメントから:

CLUSTERは、指定されたインデックスのインデックススキャン、または(インデックスがBツリーの場合)順次スキャンとその後のソートのいずれかを使用して、テーブルを再ソートできます。計画コストパラメータと利用可能な統計情報に基づいて、より高速な方法を選択しようとします。

labelDateのインデックスはbtreeです。

参照:

http://www.postgresql.org/docs/9.1/static/sql-cluster.html


「WHERE "labelDate" BETWEEN '2000-01-01' and '2020-01-01'」のような条件でも、順次スキャンが必要です。
チャーリークラーク

現時点でのクラスタリング(データはおおよその順序で入力されていますが)。それでも、WHERE句を使用してもインデックスを使用しないというクエリプランナーの決定を実際に説明するものではありません。
チャーリークラーク

セッションの順次スキャンも無効にしようとしましたか?set enable_seqscan=offいずれにせよ、ドキュメントは明確です。クラスター化すると、順次スキャンが実行されます。
Fabrizio Mazzoni

はい、シーケンシャルスキャンを無効にしてみましたが、それほど大きな違いはありませんでした。このクエリの速度は、実際のクエリでJOINSに使用できるルックアップテーブルを作成するために使用するため、実際には重要ではありません。
チャーリークラーク
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.