行の見積もりが大幅に不正確であるため、フルテキスト検索が遅い


10

このデータベースに対するフルテキストクエリ(RT(リクエストトラッカー)チケットを格納する)は、実行に非常に長い時間がかかるようです。添付ファイルテーブル(フルテキストデータを含む)は約15GBです。

データベーススキーマは次のとおりで、約200万行です。

rt4 =#\ d +添付ファイル
                                                    テーブル "public.attachments"
     コラム| タイプ| 修飾子| ストレージ| 説明文
----------------- + ----------------------------- +- -------------------------------------------------- ------ + ---------- + -------------
 id | 整数| nullではないデフォルトのnextval( 'attachments_id_seq' :: regclass)| プレーン|
 transactionid | 整数| nullではない| プレーン|
 親| 整数| nullではないデフォルト0 | プレーン|
 メッセージID | キャラクター変化(160)| | 拡張|
 件名| 文字の変化(255)| | 拡張|
 ファイル名| 文字の変化(255)| | 拡張|
 contenttype | 文字の変化(80)| | 拡張|
 contentencoding | 文字の変化(80)| | 拡張|
 コンテンツ| テキスト| | 拡張|
 ヘッダー| テキスト| | 拡張|
 クリエーター| 整数| nullではないデフォルト0 | プレーン|
 作成しました| タイムゾーンなしのタイムスタンプ| | プレーン|
 contentindex | tsvector | | 拡張|
インデックス:
    "attachments_pkey" PRIMARY KEY、btree(id)
    "attachments1" btree(親)
    "attachments2" btree(トランザクションID)
    "attachments3" btree(親、transactionid)
    「contentindex_idx」ジン(contentindex)
OIDあり:いいえ

次のようなクエリを使用して、データベース自体を非常にすばやく(1秒未満で)クエリできます。

select objectid
from attachments
join transactions on attachments.transactionid = transactions.id
where contentindex @@ to_tsquery('frobnicate');

ただし、RTが同じテーブルで全文索引検索を実行することになっているクエリを実行すると、通常、完了するまでに数百秒かかります。クエリ分析の出力は次のとおりです。

クエリ

SELECT COUNT(DISTINCT main.id)
FROM Tickets main
JOIN Transactions Transactions_1 ON ( Transactions_1.ObjectType = 'RT::Ticket' )
                                AND ( Transactions_1.ObjectId = main.id )
JOIN Attachments Attachments_2 ON ( Attachments_2.TransactionId = Transactions_1.id )
WHERE (main.Status != 'deleted')
AND ( ( ( Attachments_2.ContentIndex @@ plainto_tsquery('frobnicate') ) ) )
AND (main.Type = 'ticket')
AND (main.EffectiveId = main.id);

EXPLAIN ANALYZE 出力

                                                                             クエリプラン 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 集計(コスト= 51210.60..51210.61行= 1幅= 4)(実際の時間= 477778.806..477778.806行= 1ループ= 1)
   ->ネストされたループ(コスト= 0.00..51210.57行= 15幅= 4)(実際の時間= 17943.986..477775.174行= 4197ループ= 1)
         ->ネストされたループ(コスト= 0.00..40643.08行= 6507幅= 8)(実際の時間= 8.526..20610.380行= 1714818ループ= 1)
               ->チケットのメインのシーケンススキャン(コスト= 0.00..9818.37行= 598幅= 8)(実際の時間= 0.008..256.042行= 96990ループ= 1)
                     フィルター:((((status):: text 'deleted' :: text)AND(id = effectiveid)AND((type):: text = 'ticket' :: text))
               ->トランザクションtransactions_1(cost = 0.00..51.36 rows = 15 width = 8)のトランザクション1を使用したインデックススキャン(実際の時間= 0.102..0.202行= 18ループ= 96990)
                     インデックス条件:(((objecttype):: text = 'RT :: Ticket' :: text)AND(objectid = main.id))
         ->添付ファイルattachments_2のattachments2を使用したインデックススキャン(コスト= 0.00..1.61行= 1幅= 4)(実際の時間= 0.266..0.266行= 0ループ= 1714818)
               インデックス条件:(transactionid = transaction_1.id)
               フィルター:(contentindex @@ plainto_tsquery( 'frobnicate' :: text))
 合計実行時間:477778.883 ms

私の知る限り、問題はcontentindexフィールド(contentindex_idx)で作成されたインデックスを使用しておらず、添付ファイルテーブル内の一致する多数の行に対してフィルターを実行していることです。ANALYZEEXPLAIN 出力の行数も、最近の推定行数= 6507の実際の行数= 1714818になった後でも、非常に不正確であるように見えます。

これで次にどこへ行くのか本当に分かりません。


アップグレードすると、さらにメリットがあります。他に多くの特に一般的な改善、の:9.2は、インデックスのみスキャンおよびスケーラビリティの改善を可能にします。次の9.4では、GINインデックスに大幅な機能強化が行われます。
Erwin Brandstetter 2014

回答:


5

これは1,000通りの方法で改善でき、ミリ秒単位になるはずです。

より良いクエリ

これはエイリアスで再フォーマットされたクエリであり、霧を取り除くためにいくつかのノイズが削除されています:

SELECT count(DISTINCT t.id)
FROM   tickets      t
JOIN   transactions tr ON tr.objectid = t.id
JOIN   attachments  a  ON a.transactionid = tr.id
WHERE  t.status <> 'deleted'
AND    t.type = 'ticket'
AND    t.effectiveid = t.id
AND    tr.objecttype = 'RT::Ticket'
AND    a.contentindex @@ plainto_tsquery('frobnicate');

クエリの問題のほとんどは、最初の2つのテーブルticketsとにありtransactions、これらは質問にありません。私は教育を受けた推測で埋めています。

  • t.statust.objecttypeそしてtr.objecttypeおそらくそうではありませんがtextenumルックアップテーブルを参照する非常に小さな値である可能性があります。

EXISTS 準結合

tickets.id主キーであると仮定すると、この書き換えられたフォームははるかに安価になるはずです。

SELECT count(*)
FROM   tickets t
WHERE  status <> 'deleted'
AND    type = 'ticket'
AND    effectiveid = id
AND    EXISTS (
   SELECT 1
   FROM   transactions tr
   JOIN   attachments  a ON a.transactionid = tr.id
   WHERE  tr.objectid = t.id
   AND    tr.objecttype = 'RT::Ticket'
   AND    a.contentindex @@ plainto_tsquery('frobnicate')
   );

2つの1:n結合で行を乗算するのではなく、最後にで複数の一致を折りたたむだけで、最初の一致が見つかると同時に検索を停止し、同時に最後の手順を廃止count(DISTINCT id)するEXISTSセミ結合を使用しますDISTINCTドキュメントごと:

サブクエリは通常、少なくとも1つの行が返されるかどうかを判断するのに十分な時間だけ実行され、最後まで実行されません。

有効性は、チケットあたりのトランザクション数とトランザクションあたりの添付ファイルの数によって異なります。

結合の順序を決定 join_collapse_limit

の検索語が非常に選択的であることがわかっている場合-クエリの他の条件よりも選択的(おそらく「問題」ではなく「フロブニケート」の場合)である場合、結合のシーケンスを強制できます。クエリプランナーは、最も一般的なものを除いて、特定の単語の選択性をほとんど判断できません。ドキュメントごと:attachments.contentindex

join_collapse_limitinteger

[...]
クエリプランナーは常に最適な結合順序を選択するわけではないため、上級ユーザーはこの変数を一時的に1に設定してから、希望する結合順序を明示的に指定できます。

SET LOCAL現在のトランザクションにのみ設定する目的で使用します。

BEGIN;
SET LOCAL join_collapse_limit = 1;

SELECT count(DISTINCT t.id)
FROM   attachments  a                              -- 1st
JOIN   transactions tr ON tr.id = a.transactionid  -- 2nd
JOIN   tickets      t  ON t.id = tr.objectid       -- 3rd
WHERE  t.status <> 'deleted'
AND    t.type = 'ticket'
AND    t.effectiveid = t.id
AND    tr.objecttype = 'RT::Ticket'
AND    a.contentindex @@ plainto_tsquery('frobnicate');

ROLLBACK; -- or COMMIT;

WHERE条件の順序は常に関係ありません。ここでは、結合の順序のみが関係します。

または、「オプション2」で説明されている@jjanesのような CTEを使用します同様の効果のため。

インデックス

Bツリーインデックス

上のすべての条件取るticketsほとんどのクエリと同じように使用されており、作成部分インデックスをtickets

CREATE INDEX tickets_partial_idx
ON tickets(id)
WHERE  status <> 'deleted'
AND    type = 'ticket'
AND    effectiveid = id;

条件の1つが可変の場合は、条件から削除し、WHERE代わりに列をインデックス列として追加します。

別のものtransactions

CREATE INDEX transactions_partial_idx
ON transactions(objecttype, objectid, id)

3番目の列は、インデックスのみのスキャンを有効にするためのものです。

また、次の2つの整数列を持つこの複合インデックスがあるのでattachments

"attachments3" btree (parent, transactionid)

この追加のインデックスは完全に無駄です。削除してください:

"attachments1" btree (parent)

詳細:

GINインデックス

transactionidGINインデックスに追加すると、より効果的になります。これは別の特効薬かもしれません。インデックスのみのスキャンが可能になり、大きなテーブルへのアクセスが完全になくなるためです。
追加モジュールによって提供される追加の演算子クラスが必要ですbtree_gin。詳細な手順:

"contentindex_idx" gin (transactionid, contentindex)

integer列からの4バイトは、インデックスを大きくしません。また、幸いにも、GINインデックスは、Bツリーインデックスとは重要な点で異なります。ドキュメントごと:

複数列の GINインデックスは、インデックスの列のサブセットを含むクエリ条件で使用でき ます。BツリーやGiSTとは異なり、クエリ条件が使用するインデックス列に関係なく、インデックス検索の有効性は同じです。

大胆な強調鉱山。したがって、必要なのは1つの(大きくて多少コストがかかる)GINインデックスだけです。

テーブル定義

integer not null columns前に移動します。これは、ストレージとパフォーマンスにいくつかの小さなプラスの影響を与えます。この場合、行ごとに4〜8バイトを節約します。

                      Table "public.attachments"
         Column      |            Type             |         Modifiers
    -----------------+-----------------------------+------------------------------
     id              | integer                     | not null default nextval('...
     transactionid   | integer                     | not null
     parent          | integer                     | not null default 0
     creator         | integer                     | not null default 0  -- !
     created         | timestamp                   |                     -- !
     messageid       | character varying(160)      |
     subject         | character varying(255)      |
     filename        | character varying(255)      |
     contenttype     | character varying(80)       |
     contentencoding | character varying(80)       |
     content         | text                        |
     headers         | text                        |
     contentindex    | tsvector                    |

3

オプション1

計画者は、EffectiveIdとidの関係の本質について何の洞察も持っていないため、おそらく次の節を考えます。

main.EffectiveId = main.id

実際よりも選択性が高くなります。これが私の考えでは、EffectiveIDはほぼ常にmain.idと同じですが、プランナーはそれを知りません。

このタイプの関係を格納するためのおそらくより良い方法は、「IDと事実上同じ」を意味するEffectiveIDのNULL値を定義し、違いがある場合にのみ何かを格納することです。

スキーマを再編成したくない場合、その句を次のように書き直すことで回避できます。

main.EffectiveId+0 between main.id+0 and main.id+0

計画者は、betweenが等式よりも選択性が低いと想定している可能性があり、それは現在のトラップからそれを傾けるのに十分かもしれません。

オプション2

別のアプローチは、CTEを使用することです。

WITH attach as (
    SELECT * from Attachments 
        where ContentIndex @@ plainto_tsquery('frobnicate') 
)
<rest of query goes here, with 'attach' used in place of 'Attachments'>

これにより、プランナはContentIndexを選択性のソースとして使用する必要があります。強制されると、チケットテーブルの誤解を招く列の相関関係はそれほど魅力的に見えなくなります。もちろん、誰かが「フロブニケート」ではなく「問題」を検索すると、逆効果になる可能性があります。

オプション3

不良行の見積もりをさらに調査するには、コメント化されているさまざまなAND句のすべての2 ^ 3 = 8順列で以下のクエリを実行する必要があります。これは、悪い見積もりがどこから来ているのかを理解するのに役立ちます。

explain analyze
SELECT * FROM Tickets main WHERE 
   main.Status != 'deleted' AND 
   main.Type = 'ticket' AND 
   main.EffectiveId = main.id;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.