web-dev-qa-db-ja.com

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

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

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

 rt4 =#\ d + attachments 
テーブル "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(parent)
 "attachments2" btree(transactionid)
 "attachments3" btree(親、transactionid)
 "contentindex_idx" gin(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)
- >ネストされたループ(cost = 0.00..40643.08 rows = 6507 width = 8)(実際の時間= 8.526..20610.380 rows = 1714818 loops = 1)
->チケットのメインのSeq Scan(cost = 0.00 .. 9818.37 rows = 598 width = 8)(実際の時間= 0.008..256.042 rows = 96990 loops = 1)
 Filter:(((status):: text 'deleted' :: text)AND(id = effectiveid )AND((type):: text = 'ticket' :: text))
->トランザクションtransactions_1(cost = 0.00..51.36 rows = 15 width = 8)のtransactions1を使用したインデックススキャン(実際の時間= 0.102..0.202 rows = 18 loops = 96990)
インデックス条件:(((objecttype):: text = 'RT :: Ticket' :: text)AND(objectid = main.id))
->添付ファイルattachments_2のattachments2を使用したインデックススキャン(コスト= 0.00。 .1.61 rows = 1 width = 4)(実際の時間= 0.266..0.266 rows = 0 loops = 1714818)
 Index Cond:(transactionid = transaction_1.id)
 Filter:(contentindex @@ plainto_tsquery( 'frobnicate' :: text))
合計実行時間:477778.883 ms 

私の知る限りでは、問題はcontentindexフィールド(contentindex_idx)で作成されたインデックスを使用しておらず、むしろ、添付ファイルの表。 EXPLAIN出力の行カウントも、最近のANALYZEの後でさえ、かなり不正確であるように見えます。

私はこれで次にどこへ行くか本当にわからない。

10
JamesHannah

これは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つのテーブルticketstransactionsにあり、これらは質問にありません。私は教育を受けた推測で埋めています。

  • _t.status_、_t.objecttype_および_tr.objecttype_は、おそらくtextではなく、 enum または、ルックアップテーブル。

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_を使用して結合の順序を決定する

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

_join_collapse_limit_(integer

[...]
クエリプランナーは常に最適な結合順序を選択するわけではないため、上級ユーザーはこの変数を一時的に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条件の順序はalwaysとは無関係です。ここでは、結合の順序のみが関係します。

または、CTE @ jjanesが「オプション2」で説明したように を使用して同様の効果を得ることができます。

インデックス

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番目の列は、インデックスのみのスキャンを有効にするためのものです。

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

_"attachments3" btree (parent, transactionid)
_

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

"attachments1" btree (parent)

詳細:

GINインデックス

transactionidをGINインデックスに追加して、より効果的にします。これは別のsilver bulletの可能性があります。インデックスのみのスキャンが可能になり、bigテーブルへのアクセスが完全になくなるためです。
追加モジュール _btree_gin_ によって提供される追加の演算子クラスが必要です。詳細な手順:

"contentindex_idx" gin (transactionid, contentindex)

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

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

大胆な強調鉱山。したがって、必要なのはone(大きくて多少コストがかかる)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                    |_
5

オプション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を選択性のソースとして使用する必要があります。これを強制すると、チケットテーブルの誤解を招く列の相関関係はそれほど魅力的に見えなくなります。もちろん誰かが「フロブニケート」ではなく「問題」を検索した場合、それは逆効果になるかもしれません。

オプション

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

explain analyze
SELECT * FROM Tickets main WHERE 
   main.Status != 'deleted' AND 
   main.Type = 'ticket' AND 
   main.EffectiveId = main.id;
3
jjanes