基本的なトピックと高度なトピックの両方でPostgreSQLトレーニングを教えるとき、参加者は式インデックスがどれほど強力であるかをほとんど理解していないことに気付くことがよくあります(彼らがそれらを知っている場合)。それでは、簡単な概要を説明しましょう。
したがって、タイムスタンプの範囲を含むテーブルがあるとします(はい、日付を生成できるgenerate_series関数があります):
CREATE TABLE t AS SELECT d, repeat(md5(d::text), 10) AS padding FROM generate_series(timestamp '1900-01-01', timestamp '2100-01-01', interval '1 day') s(d); VACUUM ANALYZE t;
このテーブルには、少し大きくするためのパディング列も含まれています。それでは、表に含まれている約200年から1か月だけを選択して、簡単な範囲クエリを実行してみましょう。クエリについて説明すると、次のように表示されます。
EXPLAIN SELECT * FROM t WHERE d BETWEEN '2001-01-01' AND '2001-02-01'; QUERY PLAN ------------------------------------------------------------------------ Seq Scan on t (cost=0.00..4416.75 rows=32 width=332) Filter: ((d >= '2001-01-01 00:00:00'::timestamp without time zone) AND (d <= '2001-02-01 00:00:00'::timestamp without time zone)) (2 rows)>
私のラップトップでは、これは約20msで実行されます。これはテーブル全体を最大75,000行でウォークスルーする必要があることを考えると、悪くありません。
ただし、タイムスタンプ列にインデックスを作成しましょう(ここでのすべてのインデックスは、明示的に言及されていない限り、デフォルトのタイプ、つまりbtreeです):
CREATE INDEX idx_t_d ON t (d);
それでは、クエリをもう一度実行してみましょう:
QUERY PLAN ------------------------------------------------------------------------ Index Scan using idx_t_d on t (cost=0.29..9.97 rows=34 width=332) Index Cond: ((d >= '2001-01-01 00:00:00'::timestamp without time zone) AND (d <= '2001-02-01 00:00:00'::timestamp without time zone)) (2 rows)
これは0.5msで実行されるため、約40倍高速です。しかし、それはもちろん、式インデックスではなく、列に直接作成された単純なインデックスでした。したがって、代わりに、毎月1日ごとにデータを選択して、次のようなクエリを実行する必要があると仮定します
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
ただし、EXPLAIN ANALYZEに示すように、インデックスが列自体に構築されている間に列の式を評価する必要があるため、インデックスを使用できません。
QUERY PLAN ------------------------------------------------------------------------ Seq Scan on t (cost=0.00..4416.75 rows=365 width=332) (actual time=0.045..40.601 rows=2401 loops=1) Filter: (date_part('day'::text, d) = '1'::double precision) Rows Removed by Filter: 70649 Planning time: 0.209 ms Execution time: 43.018 ms (5 rows)
したがって、これは順次スキャンを実行する必要があるだけでなく、評価も実行する必要があり、クエリ期間が43ミリ秒に増加します。
データベースは、複数の理由でインデックスを使用できません。インデックス(少なくともbtreeインデックス)は、ツリーのような構造によって提供される並べ替えられたデータのクエリに依存します。範囲クエリはその恩恵を受けることができますが、2番目のクエリ( `extract`呼び出しを使用)は利用できません。
注:もう1つの問題は、インデックスでサポートされる(つまり、インデックスで直接評価できる)演算子のセットが非常に限られていることです。また、「抽出」機能はサポートされていないため、クエリはビットマップインデックススキャンを使用して順序の問題を回避できません。
理論的には、データベースは条件を範囲条件に変換しようとする場合がありますが、それは非常に困難であり、表現に固有のものです。この場合、プランナーはテーブル内の最小/最大タイムスタンプを実際には知らないため、このような「1日あたり」の範囲を無限に生成する必要があります。したがって、データベースは試行すらしません。
しかし、データベースは条件を変換する方法を知りませんが、開発者はしばしばそうします。たとえば、
のような条件で(column + 1) >= 1000
このように書き直すのは難しくありません
column >= (1000 - 1)
これはインデックスで問題なく機能します。
しかし、たとえばクエリの例のように、そのような変換が不可能な場合はどうなりますか
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
この場合、開発者はd列の最小/最大が不明であるという同じ問題に直面する必要があり、それでも多くの範囲が生成されます。
さて、このブログ投稿は式インデックスに関するものであり、これまでのところ、列に直接構築された通常のインデックスのみを使用してきました。それでは、最初の式インデックスを作成しましょう:
CREATE INDEX idx_t_expr ON t ((extract(day FROM d))); ANALYZE t;
次に、この説明プランを提供します
QUERY PLAN ------------------------------------------------------------------------ Bitmap Heap Scan on t (cost=47.35..3305.25 rows=2459 width=332) (actual time=2.400..12.539 rows=2401 loops=1) Recheck Cond: (date_part('day'::text, d) = '1'::double precision) Heap Blocks: exact=2401 -> Bitmap Index Scan on idx_t_expr (cost=0.00..46.73 rows=2459 width=0) (actual time=1.243..1.243 rows=2401 loops=1) Index Cond: (date_part('day'::text, d) = '1'::double precision) Planning time: 0.374 ms Execution time: 17.136 ms (7 rows)
したがって、これでは最初の例のインデックスと同じ40倍の速度向上は得られませんが、このクエリははるかに多くのタプル(2401対32)を返すため、これはちょっと予想されます。さらに、それらはテーブル全体に分散しており、最初の例のようにローカライズされていません。したがって、これは2倍の高速化であり、実際の多くの場合、はるかに大きな改善が見られます。
ただし、複雑な式を含む条件にインデックスを使用できることは、ここで最も興味深い情報ではありません。これが、人々が式インデックスを作成する理由です。しかし、それだけがメリットではありません。
上記の2つの説明プラン(式インデックスの有無にかかわらず)を見ると、次のことに気付くかもしれません:
QUERY PLAN ------------------------------------------------------------------------ Seq Scan on t (cost=0.00..4416.75 rows=365 width=332) (actual time=0.045..40.601 rows=2401 loops=1) ...
QUERY PLAN ------------------------------------------------------------------------ Bitmap Heap Scan on t (cost=47.35..3305.25 rows=2459 width=332) (actual time=2.400..12.539 rows=2401 loops=1) ...
右–式インデックスを作成すると、見積もりが大幅に改善されました。インデックスがないと、生のテーブル列の統計(MCV +ヒストグラム)しかないため、データベースは式を推定する方法を知りません
EXTRACT(day FROM d) = 1
したがって、代わりに、すべての行の0.5%である等式条件のデフォルトの推定値を適用します。テーブルには73050行があるため、最終的には365行の推定値になります。実際のアプリケーションでは、はるかに悪い推定誤差が見られるのが一般的です。
ただし、インデックスを使用すると、データベースはインデックスの列に関する統計も収集します。この場合、列には式の結果が含まれます。そして、計画中に、オプティマイザーはこれに気づき、はるかに優れた見積もりを生成します。
これは大きなメリットであり、不正確な見積もりによって引き起こされた不十分なクエリプランのいくつかのケースを修正するのに役立つ可能性があります。しかし、ほとんどの人はこの便利なツールに気づいていません。
また、このツールの有用性は、9.4でJSONBデータ型が導入されて初めて向上しました。これは、JSONBドキュメントのコンテンツに関する統計を収集する唯一の方法であるためです。
JSONBドキュメントのインデックスを作成する場合、2つの基本的なインデックス戦略が存在します。ドキュメント全体にGIN/GiSTインデックスを作成できます。このように
CREATE INDEX ON t USING GIN (jsonb_column);
これにより、JSONB列の任意のパスをクエリしたり、包含演算子を使用してサブドキュメントを照合したりできます。これはすばらしいことですが、基本的な列ごとの統計しかなく、
ドキュメントとしてはあまり役に立ちません。スカラー値として扱われます(ドキュメント全体に一致したり、ドキュメントの範囲を使用したりする人は誰もいません)。
たとえば、次のように作成された式インデックス:
CREATE INDEX ON t ((jsonb_column->'id'));
特定の式にのみ役立ちます。つまり、この新しく作成されたインデックスは
に役立ちます。SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;
ただし、「value」などの他のJSONキーにアクセスするクエリには使用できません
SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';
これは、ドキュメント全体のGIN / GiSTインデックスが役に立たないということではありませんが、選択する必要があります。フォーカスされた式インデックスを作成します。これは、特定のキーをクエリするときに役立ち、式に関する統計の利点が追加されます。または、ドキュメント全体にGIN / GiSTインデックスを作成し、任意のキーに対するクエリを処理できますが、統計はありません。
ただし、この場合、両方のインデックスを同時に作成でき、データベースが個々のクエリに使用するインデックスを選択するため、ケーキを持って食べることもできます。また、式のインデックスのおかげで、正確な統計が得られます。
残念ながら、式インデックスとGIN / GiSTインデックスは異なる条件を使用しているため、ケーキ全体を食べることはできません
-- expression (btree) SELECT * FROM t WHERE jsonb_column ->> 'id' = 123; -- GIN/GiST SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';
そのため、プランナーはそれらを同時に使用することはできません。見積もり用の式インデックスと実行用のGIN/GiSTです。