web-dev-qa-db-ja.com

PostgreSQLのパフォーマンス(col = valueまたはcolはNULL)

この質問では、PostgreSQL 9.5のクエリパフォーマンスを扱います。

テーブルは次のとおりです。

_CREATE TABLE big_table
(
   id integer NOT NULL,
   flag bigint NOT NULL,
   time timestamp with timezone NOT NULL,
   val int,
   primary key (id)
)
_

クエリを考えてみましょう:

_SELECT * 
FROM big_table 
WHERE time > '0666-06-06 00:00:00+00' AND 
      flag & 2 = 2 AND 
      (val = 5 or val IS NULL) 
ORDER by time desc
_

目標は、ユニオンを使用せずに、このクエリのパフォーマンスを可能な限り向上させることです。私はインデックスを使用して最高のパフォーマンスを達成しました:

_CREATE INDEX ind 
ON big_table USING btree (time DESC, val)
WHERE (flag & 2::smallint = 2::smallint);
_

これにより、クエリはインデックスを2回使用し、ビットマップまたはその間を使用します。これは、私がval = ANY('{NULL,5}')を実行できる場合よりもはるかに悪いです。これは不可能です。

これにより、NULLの代わりに-1などの特別な値を配置することを検討できます。これにより、val = ANY('{-1,5}')を実行して、より良いパフォーマンスでフルインデックススキャンを実行できます。

そのような状況では、NULLの代わりにいくつかの特別な値を使用する方が良いでしょうか?または、PostgreSQLは、NULL以外の値とNULLの間で「OR」を実行したい場合に最適化されていますか?

私はこの素晴らしい記事を読みましたが、ユニオンを使用せずに私が尋ねたばかりの解決策はありませんでした: https://www.cybertec-postgresql.com/en/avoid-or-for-better-performance /

私はStack Overflowとここでもそのような質問を検索しましたが、関連する質問はパフォーマンスについて話さないか、NULLとNULL以外について話さないかのどちらかです。この同様の質問(および受け取った回答)は、PostgreSQLではなくSQL Serverについて話しました: 列に非NULL値またはNULLがあるかどうかをチェックするSQLクエリを作成する最良の方法

また、nullを取り除くために合体を行うことも検討しましたが、パフォーマンスが大幅に低下することを読みました。

テーブルには400万以上のレコードがあります。それらの中から、val列にNULLがあるのは56レコードだけです。

更新:元の説明:

_"Limit  (cost=1112.32..1112.32 rows=2 width=519)"
"  ->  Sort  (cost=1112.32..1112.32 rows=2 width=519)"
"        Sort Key: time DESC"
"        ->  Bitmap Heap Scan on big_table  (cost=1104.29..1112.31 rows=2 width=519)"
"              Recheck Cond: (((time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND (val = 3) AND ((flag & '2'::smallint) = '2'::smallint)) OR ((time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND (val IS NULL) AND ((flag & '2'::smallint) = '2'::smallint)))"
"              ->  BitmapOr  (cost=1104.29..1104.29 rows=2 width=0)"
"                    ->  Bitmap Index Scan on ind  (cost=0.00..552.14 rows=1 width=0)"
"                          Index Cond: ((time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND (val = 3))"
"                    ->  Bitmap Index Scan on ind  (cost=0.00..552.14 rows=1 width=0)"
"                          Index Cond: ((time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND (val IS NULL))"
_

_(val = 3 or val IS NULL)_をcoalesce(val,-1) = ANY('{3,-1}')に変更してインデックスを作成した後に説明します。

_CREATE INDEX ind2 ON big_table USING btree (coalesce(val,-1), time DESC) WHERE (flag & 2::smallint = 2::smallint);
_

です:

_"Limit  (cost=863.45..863.50 rows=20 width=519)"
"  ->  Sort  (cost=863.45..863.99 rows=216 width=519)"
"        Sort Key: time DESC"
"        ->  Bitmap Heap Scan on big_table  (cost=11.08..857.71 rows=216 width=519)"
"              Recheck Cond: ((COALESCE(val, '-1'::integer) = ANY ('{3,-1}'::integer[])) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND ((flag & '2'::smallint) = '2'::smallint))"
"              ->  Bitmap Index Scan on ind2  (cost=0.00..11.03 rows=216 width=0)"
"                    Index Cond: ((COALESCE(val, '-1'::integer) = ANY ('{3,-1}'::integer[])) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone))"
_

元のクエリを実行しながら、元のインデックスの時間とval列の順序を変更すると、以下のより優れた説明計画が得られます。

_"Limit  (cost=16.92..16.92 rows=2 width=519)"
"  ->  Sort  (cost=16.92..16.92 rows=2 width=519)"
"        Sort Key: time DESC"
"        ->  Bitmap Heap Scan on big_table  (cost=8.89..16.91 rows=2 width=519)"
"              Recheck Cond: (((val = 3) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND ((flag & '2'::smallint) = '2'::smallint)) OR ((val IS NULL) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND ((flag & '2'::smallint) = '2'::smallint)))"
"              ->  BitmapOr  (cost=8.89..8.89 rows=2 width=0)"
"                    ->  Bitmap Index Scan on ind  (cost=0.00..4.44 rows=1 width=0)"
"                          Index Cond: ((val = 3) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone))"
"                    ->  Bitmap Index Scan on ind  (cost=0.00..4.44 rows=1 width=0)"
"                          Index Cond: ((val IS NULL) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone))"
_

そして、実際には、合体クエリを実行すると、実際にはクエリが遅くなります(列が逆になっていても、インデックスの合体で置き換えられています)。

2
Guy L.

NULLをサロゲート値に置き換えると、多少の効果がある場合とない場合があります。 NULLの処理は、インデックスでは少しコストがかかりますが、その一方で、通常はストレージが小さくなり、パフォーマンスにも影響します。

私はこれからのはるかに大きな影響を期待します:インデックス式の順序を反転します

_CREATE INDEX ON big_table (val, time DESC)
WHERE flag & 2::smallint = 2::smallint;
_

経験則:最初に同等性のインデックス、次に範囲のインデックス。見る:

ご安心ください:val = ANY('{-1,5}')は、_(val = -1 OR val = 5)_の構文の省略形であることに焼き付きます。これは、_(val IS NULL OR val = 5)_よりも優れています。 (より重要な要素は、NULLと_-1_の行数です。NULLを_-1_に置き換えるだけの場合も同じです。).

Postgresの現在のバージョンへのアップグレードも検討してください。 9.5は古くなっており、大きなテーブルのパフォーマンスがさまざまに改善されています。

少数の列のみを返すには、 index-only scan が可能な最適化ですが、コメントによると、21列のほとんどを返す必要があります。

行ごとに8バイトを節約するために、アライメントパディングで不必要に失われるため、次のようにデモテーブルの列を並べ替えます。

_CREATE TABLE big_table (
   flag bigint NOT NULL,
   time timestamp with timezone NOT NULL,
   id integer PRIMARY KEY,
   val int
);
_

全体的に小さいほど高速です。さて、それは明らかに単なるデモテーブルですが、同じ原則が実際のテーブルにも適用されます。見る:

ソート

単一のvalの場合、Postgresは事前にソートされたデータをインデックスから直接返すことができます。しかし、複数の値の場合、同じように多くの並べ替えられたサブセットをマージする必要があります(1つの並べ替えられたセットは_val IS NULL_用で、別の例では_val = 5_用です)。したがって、インデックスアクセスの上にある別の並べ替え手順は次のとおりです。必然。インデックスから事前にソートされたセットを使用すると、さらに安価になる可能性があります。どの場合でも、ソートされたインデックスタプルが必要です。実際のクエリプランは、選択したインデックスアクセス方法にも依存します。 index scan(またはindex-only scan)から事前にソートされたデータを返すのは簡単です。 ビットマップインデックススキャンではそれほどではありません。

特殊なケース:非常に少ないNULL値、常に使用

数百万行のNULL値はほんの少ししか言及しなかったので、その特別な場合のために調整されたインデックスを追加します。

_CREATE INDEX ON big_table (time DESC)
WHERE flag & 2::smallint = 2::smallint
AND   val IS NULL;
_

たぶん、この非常に小さな特別なインデックスに他のすべての関心のある列を追加して、インデックスのみのスキャンを取得することもできます。 (前提条件に依存します。)Postgres 11以降では、INCLUDE句を使用した真のカバリングインデックスでさらにそうです。これと他のインデックスからの結果は、同じインデックスからの複数のサブセットの場合と同じように、BitmapOrノードにマージされます。 Postgresは特殊なケースの正確な見積もりを持っているため、特殊なケースがNULLか-1かどうかに関係なく、完全に無関係になります。 (そもそもそれほど重要なことではありません。)参照:

3