このデータベースに対するフルテキストクエリ(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
の後でさえ、かなり不正確であるように見えます。
私はこれで次にどこへ行くか本当にわからない。
これは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.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」で説明したように を使用して同様の効果を得ることができます。
ほとんどのクエリと同じように使用される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)
詳細:
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 |
_
オプション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;