web-dev-qa-db-ja.com

IS NULLのPostgres部分インデックスが機能しない

Postgresバージョン

PostgreSQL 10.3の使用。

テーブル定義

CREATE TABLE tickets (
  id bigserial primary key,
  state character varying,
  closed timestamp
);

CREATE INDEX  "state_index" ON "tickets" ("state")
  WHERE ((state)::text = 'open'::text));

カーディナリティ

テーブルには1027616行が含まれ、そのうち51533行はstate = 'open'およびclosed IS NULL、つまり5%です。

stateに条件を含むクエリは、期待どおりにインデックススキャンを使用して適切に実行されます。

explain analyze select * from tickets where state = 'open';

Index Scan using state_index on tickets  (cost=0.29..16093.57 rows=36599 width=212) (actual time=0.025..22.875 rows=37346 loops=1)
Planning time: 0.212 ms
Execution time: 25.697 ms

条件closed IS NULLを使用したクエリで同じかそれ以上のパフォーマンスを達成しようとしています。これにより、state列を削除し、closed列を使用して同じ行をフェッチできます。 closedは、state = 'open'と同じ行のnullであるため、state列は冗長です。

select * from tickets where closed IS NULL;

ただし、私が試したどのインデックスも、最初のクエリのように単一のインデックススキャンになりません。以下は、EXPLAIN ANALYZEの結果とともに私が試したインデックスです。

部分インデックス:

CREATE INDEX  "closed_index" ON "tickets"  ("closed") WHERE (closed IS NULL)

explain analyze select * from tickets where closed IS NULL;

Bitmap Heap Scan on tickets  (cost=604.22..38697.91 rows=36559 width=212) (actual time=12.879..48.780 rows=37348 loops=1)
  Recheck Cond: (closed IS NULL)
  Heap Blocks: exact=14757
  ->  Bitmap Index Scan on closed_index  (cost=0.00..595.09 rows=36559 width=0) (actual time=7.585..7.585 rows=37348 loops=1)
Planning time: 4.831 ms
Execution time: 52.068 ms

式インデックス:

CREATE INDEX  "closed_index" ON "tickets" ((closed IS NULL))

explain analyze select * from tickets where closed IS NULL;

Seq Scan on tickets  (cost=0.00..45228.26 rows=36559 width=212) (actual time=0.025..271.418 rows=37348 loops=1)
  Filter: (closed IS NULL)
  Rows Removed by Filter: 836578
Planning time: 7.992 ms
Execution time: 274.504 ms

部分式インデックス:

CREATE INDEX  "closed_index" ON "tickets" ((closed IS NULL))
  WHERE (closed IS NULL);

explain analyze select * from tickets where closed IS NULL;

Bitmap Heap Scan on tickets  (cost=604.22..38697.91 rows=36559 width=212) (actual time=177.109..238.008 rows=37348 loops=1)
  Recheck Cond: (closed IS NULL)
  Heap Blocks: exact=14757
  ->  Bitmap Index Scan on "closed_index"  (cost=0.00..595.09 rows=36559 width=0) (actual time=174.598..174.598 rows=37348 loops=1)
Planning time: 23.063 ms
Execution time: 241.292 ms

更新しました

拡張されたテーブル定義:

CREATE TABLE tickets (
  id bigserial primary key,
  state character varying,
  closed timestamp,
  created timestamp,
  updated timestamp,
  title character varying,
  size integer NOT NULL,
  comment_count integer NOT NULL
);

CREATE INDEX  "state_index" ON "tickets" ("state")
  WHERE ((state)::text = 'open'::text));

カーディナリティ:

テーブルには1027616行が含まれており、51533行がstate = 'open'およびclosed IS NULL、または5%です。上記のように、state列を削除しようとしています。代わりに、closed列の条件を使用して同じ行をフェッチできるようにします。

state列に条件があるクエリは、インデックススキャンを使用します。

explain analyze select id, title, created, closed, updated from tickets where state = 'open';

Index Scan using state_index on tickets  (cost=0.29..22901.58 rows=49356 width=72) (actual time=0.107..49.599 rows=51533 loops=1)
Planning time: 0.511 ms
Execution time: 54.366 ms

closed列のクエリに切り替えると、同じパフォーマンス(理想的にはインデックススキャン)が必要です。 idclosed IS NULLで部分インデックスを試しました:

CREATE INDEX closed_index ON tickets (id) WHERE closed IS NULL;

VACUUM ANALYZE tickets;

explain analyze select id, title, created, closed, updated from tickets where closed IS NULL;

Bitmap Heap Scan on tickets  (cost=811.96..33999.42 rows=49461 width=72) (actual time=7.868..47.080 rows=51537 loops=1)
  Recheck Cond: (closed IS NULL)
  Heap Blocks: exact=17479
  ->  Bitmap Index Scan on closed_index  (cost=0.00..799.60 rows=49461 width=0) (actual time=4.868..4.868 rows=51537 loops=1)
Planning time: 0.222 ms
Execution time: 51.028 ms
3
GeekJock

中心的な情報を想定します。

行の約15%が_state = 'open'_および_closed IS NULL_を含む

すべての1031584行の同じ15%がこれらの両方の条件を満たすことを意味します (すべての詳細が重要です!)。両方の条件は同等に実行する必要があります-155k行(!)

クエリプランには、37346の条件を満たす行が表示されます。15%ではなく3.6%です。あなたの質問ではまだ何かが正しくありません。

3.6%の場合、インデックスは意味を成すようになります。小さな行サイズは、行あたり最大52バイト、ページあたり約155行を占めます。完全にランダムな分布では、ページあたり5〜6ヒットになります。 Postgresはとにかくすべてのページを読み、順次スキャン最速の計画である必要があります。ミスのフィルタリングは、何らかの方法でインデックスを含めるよりも高速である必要があります。

通常、対象となる行は多かれ少なかれクラスター化されており、含まれるデータページの数が少ないほど、インデックスを含める意味が大きくなります。すべてのビットマップインデックススキャンですが、インデックススキャンのケースはほとんどありません。あなたが主張する15%の場合、はるかに少ない(「ほとんどない」よりもはるかに「少ない」ことができる限り)。

あなたのupdated数(すべての行の約5%が一致)については、インデックススキャンよりもビットマップインデックススキャンを期待しています。考えられる説明:大量のデッドタプルを含むテーブルブロート。書き込み負荷が高いとおっしゃいました。その結果、データページあたりのライブタプルが少なくなり、ビットマップインデックススキャンと比較して、インデックススキャンが優先されます。 _VACUUM FULL ANALYZE_の後で最初のクエリを再テストする場合があります(テーブルに排他ロックをかける余裕がある場合!)。私の仮説が当てはまる場合、物理テーブルのサイズが大幅に縮小し、インデックススキャンではなくビットマップインデックススキャンが表示されます(より高速でもあります)。

より積極的なautovacuum設定が必要になる場合があります。見る:

部分インデックス

「式インデックス」と「部分式インデックス」は役に立ちません。実際のインデックス式として_closed IS NULL_は必要ありません(ここでは常にtrueです)。この式はコストを追加するだけで、利益はありません。

最初のプレーンな部分インデックスは、より便利なバリアントです。ただし、closedをインデックス式として使用しないでください(ここでも、常にNULLを使用します)。代わりに、他のクエリに役立つ可能性のある列を使用します。追加のコストとインデックスの膨張を避けるために、理想的には更新しないでください。主キー列idは、他の有用なアプリケーションがない場合の自然な候補です。

_CREATE INDEX closed_index ON tickets (id) WHERE closed IS NULL;
_

またはidが役に立たない場合は、代わりに定数を検討してください:

_CREATE INDEX closed_index ON tickets ((1)) WHERE closed IS NULL;
_

これにより、他の廃止されたバリアントのように実際のインデックス列が役に立たなくなりますが、追加のコストと依存関係がすべて回避されます。関連:

私がするかもしれないことは何をしようとしますか:

更新された質問に更新されます-問題の行に他に多くの書き込みがない場合にのみ意味があります(追加された列updatedと_comments_count_は私を疑わせます)。

* idとその他の関連列(few&small)を使用して部分インデックスをインデックス式として作成し、適切なクエリを使用してインデックスのみのスキャンを取得します。*

_CREATE INDEX closed_index ON tickets (id, title, created, updated)
WHERE closed IS NULL;

VACUUM ANALYZE tickets;   -- just to prove idx-only is possible

SELECT id, title, created, updated
     , NULL::timestamp AS closed  -- redundant, rather drop it
FROM   tickets
WHERE  closed IS NULL;
_

_SELECT *_は必要ありません。_closed IS NULL_はWHERE句で指定します。したがって、小さな部分インデックスを高速インデックスのみのスキャンで使用できます-前提条件を満たしていると想定します(そのため、可視性を更新するためにVACUUMをスローしましたそこにマップします)。これはまれなケースで、全行の5%以上を読み取るクエリが(テーブル全体を含めるまでも)引き続きインデックスを喜んで使用します。

設計に冗長性があるようですが、簡略化が可能なはずです。

これはPostgres 9.6以降で機能します リリースノートを引用:

  • インデックスのWHERE句がインデックス付けされていない列を参照する場合、部分インデックスで index-only scan の使用を許可します(Tomasフォンドラ、堀口京太郎)

    たとえば、CREATE INDEX tidx_partial ON t(b) WHERE a > 0で定義されたインデックスは、_WHERE a > 0_を指定し、aを使用しないクエリによるインデックスのみのスキャンに使用できるようになりました。以前は、aがインデックス列としてリストされていないため、これは許可されていませんでした。

または、質問の情報が誤解を招く可能性があります。

関連:

これでインデックスのみのスキャンが表示されない場合は、VACUUMを実行した直後でも、書き込みの負荷が高く、可視性マップがインデックスのみのスキャンを許可する状態になることはありません。 マニュアル。 または、VACUUMがその仕事をしないようにするDBに別の問題があります。関連:

8