web-dev-qa-db-ja.com

pg_trgmインデックスを使用した類似検索のクエリ時間が遅い

2つのpg_trgmインデックスをテーブルに追加しました。これは、ユーザー名、またはサインアップ中にスペルが間違っているメールアドレス(例: "@ gmail.con")でユーザーを検索する必要があるため、メールアドレスまたは名前によるあいまい検索を可能にします。 ANALYZEはインデックス作成後に実行されました。

ただし、これらのインデックスのいずれかでランク付けされた検索を実行すると、ほとんどの場合非常に遅くなります。つまり、タイムアウトが増加すると、クエリmightは60秒で戻りますが、15秒という非常にまれなケースですが、通常はクエリがタイムアウトします。

_pg_trgm.similarity_threshold_は_0.3_のデフォルト値ですが、これを_0.8_に上げても違いはないようです。

この特定のテーブルには2,500万行以上があり、常に照会、更新、および挿入されます(それぞれの平均時間は2ミリ秒未満です)。セットアップは、汎用SSDストレージと多かれ少なかれデフォルトのパラメーターを備えたRDS db.m4.largeインスタンスで実行されるPostgreSQL 9.6.6です。 pg_trgm拡張はバージョン1.3です。

クエリ:

  • _SELECT *
    FROM users
    WHERE email % '[email protected]'
    ORDER BY email <-> '[email protected]' LIMIT 10;
    _
  • _SELECT *
    FROM users
    WHERE (first_name || ' ' || last_name) % 'chris orr'
    ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;
    _

これらのクエリはそれほど頻繁に実行する必要はありませんが(1日数十回)、現在のテーブルの状態に基づいている必要があり、理想的には約10秒以内に返されます。


スキーマ:

_=> \d+ users
                                          Table "public.users"
          Column   |            Type             | Collation | Nullable | Default | Storage  
-------------------+-----------------------------+-----------+----------+---------+----------
 id                | uuid                        |           | not null |         | plain    
 email             | citext                      |           | not null |         | extended 
 email_is_verified | boolean                     |           | not null |         | plain    
 first_name        | text                        |           | not null |         | extended 
 last_name         | text                        |           | not null |         | extended 
 created_at        | timestamp without time zone |           |          | now()   | plain    
 updated_at        | timestamp without time zone |           |          | now()   | plain    
 …                 | boolean                     |           | not null | false   | plain    
 …                 | character varying(60)       |           |          |         | extended 
 …                 | character varying(6)        |           |          |         | extended 
 …                 | character varying(6)        |           |          |         | extended 
 …                 | boolean                     |           |          |         | plain    
Indexes:
  "users_pkey" PRIMARY KEY, btree (id)
  "users_email_key" UNIQUE, btree (email)
  "users_search_email_idx" Gist (email Gist_trgm_ops)
  "users_search_name_idx" Gist (((first_name || ' '::text) || last_name) Gist_trgm_ops)
  "users_updated_at_idx" btree (updated_at)
Triggers:
  update_users BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_modified_column()
Options: autovacuum_analyze_scale_factor=0.01, autovacuum_vacuum_scale_factor=0.05
_

(_users_search_name_idx_にunaccent()と名前クエリも追加する必要があることは承知しています...)


説明:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE (first_name || ' ' || last_name) % 'chris orr' ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;

_Limit  (cost=0.42..40.28 rows=10 width=152) (actual time=58671.973..58676.193 rows=10 loops=1)
  Buffers: shared hit=66227 read=231821
  ->  Index Scan using users_search_name_idx on users  (cost=0.42..100264.13 rows=25153 width=152) (actual time=58671.970..58676.180 rows=10 loops=1)
        Index Cond: (((first_name || ' '::text) || last_name) % 'chris orr'::text)
        Order By: (((first_name || ' '::text) || last_name) <-> 'chris orr'::text"
        Buffers: shared hit=66227 read=231821
Planning time: 0.125 ms
Execution time: 58676.265 ms
_

メール検索は名前検索よりもタイムアウトする可能性が高くなりますが、それはおそらくメールアドレスが非常に似ているためです(たとえば、@-gmail.comアドレスのlot)。

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE email % '[email protected]' ORDER BY email <-> '[email protected]' LIMIT 10;

_Limit  (cost=0.42..40.43 rows=10 width=152) (actual time=58851.719..62181.128 rows=10 loops=1)
  Buffers: shared hit=83 read=428918
  ->  Index Scan using users_search_email_idx on users  (cost=0.42..100646.36 rows=25153 width=152) (actual time=58851.716..62181.113 rows=10 loops=1)
        Index Cond: ((email)::text % '[email protected]'::text)
        Order By: ((email)::text <-> '[email protected]'::text)
        Buffers: shared hit=83 read=428918
Planning time: 0.100 ms
Execution time: 62181.186 ms
_

クエリ時間が遅い理由は何ですか?読み込まれるバッファの数と関係がありますか?私はこの特定の種類のクエリの最適化に関する多くの情報を見つけることができませんでした、そしてクエリはとにかくpg_trgmドキュメントのクエリと非常に似ています。

これは、Postgresで最適化したり、より適切に実装したりできますか、それとも、Elasticsearchのようなものをこの特定のユースケースに適していますか?

9
Christopher Orr

gin_trgm_opsではなくGist_trgm_opsを使用すると、パフォーマンスが向上する可能性があります。どちらが良いかはかなり予測できません。データとクエリ用語のテキストパターンと長さの分布に敏感です。あなたはほとんどそれを試して、それがあなたのためにどのように機能するかを見る必要があります。 GINメソッドは、Gistメソッドとは異なり、pg_trgm.similarity_thresholdに対して非常に敏感です。また、pg_trgmのバージョンによっても異なります。古いバージョンのPostgreSQLから始めてpg_upgradeで更新した場合、最新バージョンではない可能性があります。プランナーは、どのインデックスタイプが優れているかを予測することはできません。したがって、それをテストするには、両方を作成するだけではなく、もう一方をドロップして、プランナーに強制的に必要なものを使用させる必要があります。

電子メール列の特定のケースでは、それらをユーザー名とドメインに分割し、正確なドメインを使用して同様のユーザー名をクエリすることをお勧めします。逆も同様です。次に、主要なクラウドメールプロバイダーの極端な普及により、情報をほとんど追加しないトライグラムでインデックスが汚染される可能性が低くなります。

最後に、これのユースケースは何ですか?これらのクエリを実行する必要がある理由を知ることは、より良い提案につながる可能性があります。特に、メールが配信可能で正しい人物に送信されることが確認された後、メールで類似検索を実行する必要があるのはなぜですか?おそらく、まだ検証されていない電子メールのサブセットのみに部分的なインデックスを作成できますか?

1
jjanes