web-dev-qa-db-ja.com

複合BTREE + GIN_TRGM_OPSインデックスの優先順位付けと奇妙なlower()の動作を理解する

誰かがインデックスの動作を解読する手助けをしてくれることを期待しています。私はさまざまなユーザーデータ列(〜varchar <255)でいくつかの単純なcontainsタイプルックアップを有効にし、インデックスの動作を理解しようとしています。全体(たぶんフルテキストですか?-ある時点で、より広い範囲の検索アプリケーションが本当に必要になる可能性が高いと思いますが、現時点では、そのアプリケーションに移行するには時間がかかります)

Anyhoo、私の場合、主にカテゴリ/タイプのタプルで始まるこのテーブルからすべてのユーザーを検索します(Railsを使用した単一テーブルの継承のため)

Postgres11を使用したテーブルとインデックスの例:

_CREATE TABLE people (
    id SERIAL,
    email character varying(255) not null,
    first_name character varying(255) not null,
    last_name character varying(255) not null,
    user_category integer not null,
    user_type character varying(255) not null
);

-- Dummy Data
INSERT INTO people (email, first_name, last_name, user_category, user_type)
SELECT
  concat(md5(random()::text), '@not-real-email-plz.com'),
  md5(random()::text), 
  md5(random()::text), 
  ceil(random() * 3), 
  ('{Grunt,Peon,Ogre}'::text[])[ceil(random()*3)]
FROM
  (SELECT * FROM generate_series(1,1000000) AS id) AS x;

-- Standard, existing lookup
CREATE INDEX index_people_category_type ON people USING btree (user_category, user_type);

-- taken from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
CREATE INDEX idx_people_gin_user_category_and_user_type_and_full_name 
ON people
USING GIN(user_category, user_type, (first_name || ' ' || last_name) gin_trgm_ops);    

-- first name
CREATE INDEX idx_people_gin_user_category_and_user_type_and_first_name 
ON people
USING GIN(user_category, user_type, first_name gin_trgm_ops);

-- last name
CREATE INDEX idx_people_gin_user_category_and_user_type_and_last_name 
ON people
USING GIN(user_category, user_type, last_name gin_trgm_ops);

-- email
CREATE INDEX idx_people_gin_user_category_and_user_type_and_email 
ON people
USING GIN(user_category, user_type, email gin_trgm_ops);

-- non-composite email (had for testing and raised more questions)
CREATE INDEX idx_people_gin_email 
ON people
USING GIN(email gin_trgm_ops);
_

私はGINインデックスではその順序は重要ではないことを読んだので、最初の質問は、それらの使用法の任意の組み合わせで機能する複数の列を含む1つのインデックスを作成することも可能かどうかです。インデックスは確実にサイズによって異なりますが、注文の詳細の影響については確信が持てなかったので、私の推測は違います。

とにかく、私が観察したものに!

最初に気づいたことの1つは、単にカテゴリとタイプで検索すると、GINインデックスが最初のbツリーインデックスに取って代わるように見えることです。

_EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')
_

結果:

_Unique  (cost=52220.05..53334.71 rows=222932 width=4) (actual time=251.070..339.769 rows=222408 loops=1)
  Output: id
  ->  Sort  (cost=52220.05..52777.38 rows=222932 width=4) (actual time=251.069..285.652 rows=222408 loops=1)
        Output: id
        Sort Key: people.id
        Sort Method: external merge  Disk: 3064kB
        ->  Bitmap Heap Scan on public.people  (cost=3070.23..29368.23 rows=222932 width=4) (actual time=35.156..198.549 rows=222408 loops=1)
              Output: id
              Recheck Cond: (people.user_category = 2)
              Filter: ((people.user_type)::text <> 'Ogre'::text)
              Rows Removed by Filter: 111278
              Heap Blocks: exact=21277
              ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_email  (cost=0.00..3014.50 rows=334733 width=0) (actual time=32.017..32.017 rows=333686 loops=1)
                    Index Cond: (people.user_category = 2)
Planning Time: 0.293 ms
Execution Time: 359.247 ms
_

この時点で元のbツリーは完全に冗長ですか?これらのデータ型に対してBツリーの方が高速である場合、これらの2つの列のみが使用されていれば、プランナーによって引き続き選択される可能性があると予想しましたが、そうではないようです。

次に、既存のクエリがlower()に依存しており、GINインデックスを完全に無視しているようであることに気づきました。むしろ、その列がで使用されていなくても、最後に作成されたものを使用しているようですクエリ:

_EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')
  AND (LOWER(last_name) LIKE LOWER('%a62%'))
_

結果(last_nameを比較していますが、電子メールインデックスを使用しています):

_HashAggregate  (cost=28997.16..29086.33 rows=8917 width=4) (actual time=175.204..175.554 rows=1677 loops=1)
  Output: id
  Group Key: people.id
  ->  Gather  (cost=4016.73..28974.87 rows=8917 width=4) (actual time=39.947..181.936 rows=1677 loops=1)
        Output: id
        Workers Planned: 2
        Workers Launched: 2
        ->  Parallel Bitmap Heap Scan on public.people  (cost=3016.73..27083.17 rows=3715 width=4) (actual time=22.037..156.233 rows=559 loops=3)
              Output: id
              Recheck Cond: (people.user_category = 2)
              Filter: (((people.user_type)::text <> 'Ogre'::text) AND (lower((people.last_name)::text) ~~ '%a62%'::text))
              Rows Removed by Filter: 110670
              Heap Blocks: exact=7011
              Worker 0: actual time=13.573..147.844 rows=527 loops=1
              Worker 1: actual time=13.138..147.867 rows=584 loops=1
              ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_email  (cost=0.00..3014.50 rows=334733 width=0) (actual time=35.445..35.445 rows=333686 loops=1)
                    Index Cond: (people.user_category = 2)
Planning Time: 7.546 ms
Execution Time: 189.186 ms
_

一方、ILIKEに切り替える

_EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')
  AND (last_name ILIKE '%A62%')
_

結果は非常に速くなり、予想されるインデックスを使用します。プランナーがビートをスキップするように見えるlower()呼び出しについてはどうですか?

_Unique  (cost=161.51..161.62 rows=22 width=4) (actual time=27.144..27.570 rows=1677 loops=1)
  Output: id
  ->  Sort  (cost=161.51..161.56 rows=22 width=4) (actual time=27.137..27.256 rows=1677 loops=1)
        Output: id
        Sort Key: people.id
        Sort Method: quicksort  Memory: 127kB
        ->  Bitmap Heap Scan on public.people  (cost=32.34..161.02 rows=22 width=4) (actual time=16.470..26.798 rows=1677 loops=1)
              Output: id
              Recheck Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
              Filter: ((people.user_type)::text <> 'Ogre'::text)
              Rows Removed by Filter: 766
              Heap Blocks: exact=2291
              ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_last_name  (cost=0.00..32.33 rows=33 width=0) (actual time=16.058..16.058 rows=2443 loops=1)
                    Index Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
Planning Time: 10.577 ms
Execution Time: 27.746 ms
_

次に、別のフィールドを追加します...

_EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')
  AND (last_name ILIKE '%A62%')
  AND (first_name ILIKE '%EAD%')
_

全体的にまだかなりスピーディーです

_Unique  (cost=161.11..161.11 rows=1 width=4) (actual time=10.854..10.860 rows=12 loops=1)
  Output: id
  ->  Sort  (cost=161.11..161.11 rows=1 width=4) (actual time=10.853..10.854 rows=12 loops=1)
        Output: id
        Sort Key: people.id
        Sort Method: quicksort  Memory: 25kB
        ->  Bitmap Heap Scan on public.people  (cost=32.33..161.10 rows=1 width=4) (actual time=3.895..10.831 rows=12 loops=1)
              Output: id
              Recheck Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
              Filter: (((people.user_type)::text <> 'Ogre'::text) AND ((people.first_name)::text ~~* '%EAD%'::text))
              Rows Removed by Filter: 2431
              Heap Blocks: exact=2291
              ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_last_name  (cost=0.00..32.33 rows=33 width=0) (actual time=3.173..3.173 rows=2443 loops=1)
                    Index Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
Planning Time: 0.257 ms
Execution Time: 10.897 ms
_

それでも、下部に作成された追加の非タプルインデックスに戻って、メールでフィルタリングすると、別のインデックスが利用されているように見えますか?

_EXPLAIN ANALYZE VERBOSE

SELECT DISTINCT id
FROM people
WHERE user_category = 2
  AND (user_type != 'Ogre')
  AND (last_name ILIKE '%A62%')
  AND (email ILIKE '%0F9%')
_

別のパスがあります:

_Unique  (cost=140.37..140.38 rows=1 width=4) (actual time=4.180..4.184 rows=7 loops=1)
  Output: id
  ->  Sort  (cost=140.37..140.38 rows=1 width=4) (actual time=4.180..4.180 rows=7 loops=1)
        Output: id
        Sort Key: people.id
        Sort Method: quicksort  Memory: 25kB
        ->  Bitmap Heap Scan on public.people  (cost=136.34..140.36 rows=1 width=4) (actual time=4.145..4.174 rows=7 loops=1)
              Output: id
              Recheck Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text) AND ((people.email)::text ~~* '%0F9%'::text))
              Filter: ((people.user_type)::text <> 'Ogre'::text)
              Rows Removed by Filter: 4
              Heap Blocks: exact=11
              ->  BitmapAnd  (cost=136.34..136.34 rows=1 width=0) (actual time=4.125..4.125 rows=0 loops=1)
                    ->  Bitmap Index Scan on idx_people_gin_user_category_and_user_type_and_last_name  (cost=0.00..32.33 rows=33 width=0) (actual time=3.089..3.089 rows=2443 loops=1)
                          Index Cond: ((people.user_category = 2) AND ((people.last_name)::text ~~* '%A62%'::text))
                    ->  Bitmap Index Scan on idx_people_gin_email  (cost=0.00..103.76 rows=10101 width=0) (actual time=0.879..0.879 rows=7138 loops=1)
                          Index Cond: ((people.email)::text ~~* '%0F9%'::text)
Planning Time: 0.291 ms
Execution Time: 4.217 ms
_

コストは無視できるほど低いように見えますが、これによってフィルタリングできるかなり動的な量の列にとってこれは何を意味するのでしょうかすべてのフィールドにも非タプルインデックスを作成するのが理想的でしょうか?

長すぎて申し訳ありませんが、しばらくの間すべてを理解しようと私のホイールを回転させましたが、洞察はかなり素晴らしいでしょう(そして、このようなGINインデックスはたくさんないようですが、おそらくもっと何かが足りないかもしれません)基本的な全体)

1
Jeff B.

この時点で元のbツリーは完全に冗長ですか?これらのデータ型に対してBツリーの方が高速である場合、これらの2つの列のみが使用されていれば、プランナーによって引き続き選択される可能性があると予想しましたが、そうではないようです。

完全にではありません。 btreeインデックスは、順序付けに使用できます(ただし、各列に3つの異なる値がある場合、そのためにどれだけの呼び出しが必要かは明確ではありません)。私の手では、Bツリーインデックスは実際には高速ですが、それほど高速ではありません。もっと速くなると思った。 !=テストを=(bツリーが輝く場所)に変更しても、bツリーインデックスはわずかに高速です。複数列の等価性テストにおけるbtreeの利点は、TIDリストのストレージを圧縮するというGINの利点によってほとんど打ち消されました。これは、3つの値のそれぞれが333,333回を表示するときに役立ちます。これが他の状況に持ち越されることを期待しないでください。

結果は非常に速くなり、予想されるインデックスを使用します。プランナーがビートをスキップするように見えるlower()呼び出しについてはどうですか?

あなたが検索したいものにインデックスを構築する必要があります。 _(user_category, user_type, lower(last_name) gin_trgm_ops);_でインデックスを作成した場合は、それを使用します。 PostgreSQLは、lower()がテキストを受け取り、テキストを吐き出すことを知っています。 lower(a) LIKE lower(b)が_a ILIKE b_を意味するかどうかはわかりません。

コストは無視できるほど低いように見えますが、これによってフィルタリングできるかなり動的な量の列にとってこれは何を意味するのでしょうかすべてのフィールドにも非タプルインデックスを作成するのが理想的でしょうか?

タプル以外のインデックスの意味がわかりません。 N列のGINインデックスを作成することは、N個の単一列のGINインデックスを作成することとほぼ同じです。プランナは個々のインデックスをBitmapAndおよびBitmapOrと組み合わせることができ、GINはマルチカラムインデックスの複数のカラムをほとんど同じ方法で組み合わせることができますが、透過性はありません。

2
jjanes