매우 부정확 한 행 추정으로 인해 전체 텍스트 검색 속도가 느림


10

이 데이터베이스에 대한 전체 텍스트 쿼리 (RT ( Request Tracker ) 티켓 저장 )를 실행하는 데 시간이 오래 걸립니다. 첨부 파일 테이블 (전체 텍스트 데이터 포함)은 약 15GB입니다.

데이터베이스 스키마는 다음과 같습니다. 약 2 백만 개의 행입니다.

rt4 = # \ d + 첨부 파일
                                                    "public.attachments"표
     열 | 타입 | 수정 자 | 저장 | 기술
----------------- + ----------------------------- +- -------------------------------------------------- ------ + ---------- + -------------
 아이디 | 정수 | not null 기본 nextval ( 'attachments_id_seq':: regclass) | 평원 |
 transactionid | 정수 | 널이 아님 | 평원 |
 부모 | 정수 | not null 기본값 0 | 평원 |
 messageid | 문자 변화 (160) | | 확장 |
 주제 | 문자 변화 (255) | | 확장 |
 파일 이름 | 문자 변화 (255) | | 확장 |
 contenttype | 문자 변화 (80) | | 확장 |
 contentencoding | 문자 변화 (80) | | 확장 |
 내용 | 텍스트 | | 확장 |
 헤더 | 텍스트 | | 확장 |
 크리에이터 | 정수 | not null 기본값 0 | 평원 |
 작성 | 시간대없는 타임 스탬프 | | 평원 |
 contentindex | tsvector | | 확장 |
인덱스 :
    "attachments_pkey"기본 키, btree (id)
    "첨부 파일 1"btree (부모)
    "attachments2"btree (트랜잭션 ID)
    "attachments3"btree (부모, transactionid)
    "contentindex_idx"진 (contentindex)
OID가 있음 : 아니오

다음과 같은 쿼리를 사용하여 데이터베이스를 매우 빠르게 (<1s) 쿼리 할 수 ​​있습니다.

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))
               -> 거래 transaction_1에서 거래 1을 사용한 인덱스 스캔 (비용 = 0.00..51.36 행 = 15 폭 = 8) (실제 시간 = 0.102..0.202 행 = 18 루프 = 96990)
                     인덱스 조건 : (((objecttype) :: text = 'RT :: Ticket':: text) AND (objectid = main.id))
         -> 첨부 파일 첨부 파일 _2에서 첨부 파일 2를 사용한 인덱스 스캔 (비용 = 0.00..1.61 행 = 1 너비 = 4) (실제 시간 = 0.266..0.266 행 = 0 루프 = 1714818)
               인덱스 조건 : (transactionid = transaction_1.id)
               필터 : (contentindex @@ plainto_tsquery ( 'frobnicate':: text))
 총 런타임 : 477778.883ms

내가 알 수있는 한, 문제는 contentindex필드 ( contentindex_idx)에서 생성 된 색인을 사용하지 않고 첨부 파일 테이블의 많은 수의 일치하는 행에서 필터를 수행하는 것 같습니다. Explain 출력의 행 수는 최근 ANALYZE: 예상 행 = 6507 실제 행 = 1714818 이후에도 매우 부정확 한 것으로 보입니다.

나는 이것으로 다음에 어디로 갈지 확실하지 않습니다.


업그레이드하면 추가 혜택이 제공됩니다. 게다가 많은 , 특히 일반 개선의 : 9.2 인덱스 만 스캔 및 확장 성을 개선 할 수 있습니다. 다가오는 9.4는 GIN 지수를 크게 향상시킬 것입니다.
Erwin Brandstetter

답변:


5

이것은 수천 가지 방법으로 개선 될 수 있으며, 밀리 초 문제입니다 .

더 나은 쿼리

이것은 별칭으로 다시 포맷 된 쿼리이며 안개를 없애기 위해 약간의 노이즈가 제거되었습니다.

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');

쿼리 관련 문제의 대부분은 질문에서 누락 된 첫 두 테이블 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')
   );

두 개의 1 : n 조인으로 행을 곱하는 대신으로 끝나는 여러 일치 항목 만 축소 count(DISTINCT id)하려면 EXISTS세미 조인을 사용 하십시오. 첫 번째 일치 항목이 발견되는 즉시 더 이상 찾고 중지하고 마지막 DISTINCT단계를 사용하지 않습니다 . 문서 당 :

부속 조회는 일반적으로 완료 될 때까지가 아니라 하나 이상의 행이 리턴되는지 여부를 판별 할만큼 충분히 오래 실행됩니다.

유효성은 티켓 당 트랜잭션 수와 트랜잭션 당 첨부 파일 수에 따라 다릅니다.

조인 순서 결정 join_collapse_limit

당신이 경우 알고 에 대한 검색어가 있다는 attachments.contentindex것입니다 매우 선택적 - (아마,하지만 '문제'에 대한 'frobnicate'의 경우입니다) 쿼리에 다른 조건보다 더 많은 선택을, 당신은 조인의 순서를 강제 할 수 있습니다. 쿼리 플래너는 가장 일반적인 단어를 제외하고 특정 단어의 선택성을 판단 할 수 없습니다. 문서 당 :

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조건 의 순서 는 항상 관련이 없습니다. 조인 순서 만 관련이 있습니다.

또는 "옵션 2"에 설명 된 @jjanes와 같은 CTE를 사용하십시오 . 비슷한 효과.

인덱스

B- 트리 인덱스

tickets대부분의 쿼리와 동일하게 사용되는 모든 조건을 취하고 다음 에 대한 부분 인덱스 를 만듭니다 tickets.

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

조건 중 하나가 변수 인 경우 조건에서 조건을 삭제하고 WHERE대신 컬럼을 인덱스 컬럼으로 추가하십시오.

다른 하나 transactions:

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

세 번째 열은 인덱스 전용 스캔을 활성화하는 것입니다.

또한, 당신이 두 개의 정수 열이 복합 인덱스가 있기 때문에 attachments:

"attachments3" btree (parent, transactionid)

이 추가 색인은 완전한 낭비 이므로 삭제하십시오.

"attachments1" btree (parent)

세부:

진 지수

추가 transactionid가 훨씬 더 효과적으로 만들기 위해 GIN 인덱스에. 인덱스 전용 스캔을 허용하여 테이블을 완전히 방문 할 필요가 없기 때문에 이것은 또 다른 은색 총알 일 수 있습니다 . 추가 모듈에서 제공하는 추가 연산자 클래스가 필요합니다 . 자세한 지침 :
btree_gin

"contentindex_idx" gin (transactionid, contentindex)

integer열 에서 4 바이트 는 인덱스를 크게 만들지 않습니다. 또한 GIN 인덱스는 중요한 측면에서 B- 트리 인덱스와 다릅니다. 문서 당 :

다중 열 GIN 인덱스는 인덱스 열의 하위 집합 을 포함 하는 쿼리 조건과 함께 사용할 수 있습니다 . B- 트리 또는 GiST와 달리, 인덱스 검색 효과는 쿼리 조건이 사용하는 인덱스 열에 관계없이 동일 합니다.

대담한 강조 광산. 따라서 하나의 (큰 다소 비싼) 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와 동일하지만 플래너는 그것을 알지 못합니다.

이 유형의 관계를 저장하는 더 좋은 방법은 일반적으로 EffectiveID의 NULL 값을 "id와 실질적으로 동일"을 의미하도록 정의하고 차이가있는 경우에만 무언가를 저장하는 것입니다.

스키마를 재구성하지 않으려는 경우 해당 절을 다음과 같이 다시 작성하여 스키마를 해결할 수 있습니다.

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.