Tìm kiếm toàn văn bản chậm do ước tính hàng không chính xác


10

Truy vấn Fulltext đối với cơ sở dữ liệu này (lưu trữ vé RT ( Request Tracker ) dường như mất nhiều thời gian để thực hiện. Bảng đính kèm (chứa dữ liệu toàn văn bản) khoảng 15 GB.

Lược đồ cơ sở dữ liệu như sau, có khoảng 2 triệu hàng:

rt4 = # \ d + tệp đính kèm
                                                    Bảng "public.attachments"
     Cột | Loại | Công cụ sửa đổi | Lưu trữ | Sự miêu tả
----------------- + ----------------------------- + - -------------------------------------------------- ------ + ---------- + -------------
 id | số nguyên | không null mặc định nextval ('tệp đính kèm_id_seq' :: reggroup) | đồng bằng |
 giao dịch | số nguyên | không null | đồng bằng |
 cha mẹ | số nguyên | không null mặc định 0 | đồng bằng |
 tin nhắn | thay đổi ký tự (160) | | mở rộng |
 môn học | thay đổi ký tự (255) | | mở rộng |
 tên tệp | thay đổi ký tự (255) | | mở rộng |
 nội dung | thay đổi ký tự (80) | | mở rộng |
 mã hóa nội dung | thay đổi ký tự (80) | | mở rộng |
 nội dung | văn bản | | mở rộng |
 tiêu đề | văn bản | | mở rộng |
 người sáng tạo | số nguyên | không null mặc định 0 | đồng bằng |
 đã tạo | dấu thời gian không có múi giờ | | đồng bằng |
 contentindex | tsvector | | mở rộng |
Chỉ mục:
    "Tệp đính kèm_pkey" KHÓA CHÍNH, btree (id)
    "tập tin đính kèm1" btree (phụ huynh)
    "Tệp đính kèm2" btree (giao dịch)
    "Tệp đính kèm3" btree (cha mẹ, giao dịch)
    "contentindex_idx" gin (contentindex)
Có OID: không

Tôi có thể truy vấn cơ sở dữ liệu trên chính nó rất nhanh (<1s) bằng một truy vấn như:

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

Tuy nhiên, khi RT chạy một truy vấn được cho là thực hiện tìm kiếm chỉ mục toàn văn bản trên cùng một bảng, thường mất hàng trăm giây để hoàn thành. Đầu ra phân tích truy vấn như sau:

Truy vấn

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 đầu ra

                                                                             KẾ HOẠCH 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 Tổng hợp (chi phí = 51210.60..51210.61 hàng = 1 width = 4) (thời gian thực tế = 477778.806..477778.806 hàng = 1 vòng = 1)
   -> Vòng lặp lồng nhau (chi phí = 0,00..51210,57 hàng = 15 chiều rộng = 4) (thời gian thực tế = 17943.986..477775.174 hàng = 4197 vòng = 1)
         -> Vòng lặp lồng nhau (chi phí = 0,00..40643,08 hàng = 6507 chiều rộng = 8) (thời gian thực tế = 8,526..20610.380 hàng = 1714818 vòng = 1)
               -> Seq Quét trên vé chính (chi phí = 0,00..9818.37 hàng = 598 chiều rộng = 8) (thời gian thực tế = 0,008..256.042 hàng = 96990 vòng = 1)
                     Bộ lọc: (((trạng thái) :: văn bản 'đã xóa' :: văn bản) VÀ (id = hiệu quả) VÀ ((loại) :: văn bản = 'vé' :: văn bản))
               -> Quét chỉ mục bằng cách sử dụng giao dịch1 trên giao dịch giao dịch_1 (chi phí = 0,00..51.36 hàng = 15 width = 8) (thời gian thực tế = 0.102..0.202 hàng = 18 vòng = 96990)
                     Chỉ số Cond: (((objecttype) :: text = 'RT :: Ticket' :: text) AND (objectid = main.id))
         -> Quét chỉ mục bằng cách sử dụng tệp đính kèm2 trên tệp đính kèm tệp đính kèm_2 (chi phí = 0,00..1.61 hàng = 1 width = 4) (thời gian thực tế = 0.266..0.266 hàng = 0 vòng = 1714818)
               Chỉ số Cond: (giao dịch = giao dịch_1.id)
               Bộ lọc: (contentindex @@ plainto_tsquery ('frobnicate' :: text))
 Tổng thời gian chạy: 477778.883 ms

Theo như tôi có thể nói, vấn đề có vẻ là nó không sử dụng chỉ mục được tạo trên contentindextrường ( contentindex_idx), mà là nó đang thực hiện một bộ lọc trên một số lượng lớn các hàng khớp trong bảng đính kèm. Số lượng hàng trong đầu ra giải thích cũng có vẻ không chính xác, thậm chí sau một hàng gần đây ANALYZE: các hàng ước tính = 6507 hàng thực tế = 1714818.

Tôi không thực sự chắc chắn nơi tiếp theo với điều này.


Nâng cấp sẽ mang lại lợi ích bổ sung. Bên cạnh rất nhiều cải tiến chung, cụ thể: 9.2 cho phép quét chỉ mục và cải thiện khả năng mở rộng. Bản 9.4 sắp tới sẽ mang lại những cải tiến lớn cho các chỉ số GIN.
Erwin Brandstetter

Câu trả lời:


5

Điều này có thể được cải thiện theo một nghìn lẻ một cách, sau đó nó sẽ là vấn đề của một phần nghìn giây .

Truy vấn tốt hơn

Đây chỉ là truy vấn của bạn được định dạng lại với các bí danh và một số nhiễu được loại bỏ để xóa sương mù:

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

Hầu hết các vấn đề với truy vấn của bạn nằm ở hai bảng đầu tiên ticketstransactions, thiếu trong câu hỏi. Tôi đang điền vào những phỏng đoán có giáo dục.

  • t.status, t.objecttypetr.objecttypecó lẽ không nên text, nhưng enumhoặc có thể một số giá trị rất nhỏ tham khảo bảng tra cứu.

EXISTS bán tham gia

Giả sử tickets.idlà khóa chính, hình thức viết lại này sẽ rẻ hơn nhiều:

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

Thay vì nhân các hàng với hai lần tham gia 1: n, chỉ để thu gọn nhiều trận đấu cuối cùng count(DISTINCT id), hãy sử dụng một liên EXISTSkết bán kết hợp, có thể dừng tìm kiếm thêm ngay sau khi trận đấu đầu tiên được tìm thấy và đồng thời làm lỗi DISTINCTbước cuối cùng . Mỗi tài liệu:

Truy vấn con nói chung sẽ chỉ được thực hiện đủ lâu để xác định xem có ít nhất một hàng được trả về hay không, không phải là tất cả các cách để hoàn thành.

Hiệu quả phụ thuộc vào số lượng giao dịch trên mỗi vé và tệp đính kèm trên mỗi giao dịch.

Xác định thứ tự tham gia với join_collapse_limit

Nếu bạn biết rằng thuật ngữ tìm kiếm của bạn cho attachments.contentindexrất có chọn lọc - có chọn lọc hơn các điều kiện khác trong truy vấn (mà có lẽ là trường hợp cho 'frobnicate', nhưng không cho 'vấn đề'), bạn có thể buộc các chuỗi tham gia. Công cụ lập kế hoạch truy vấn khó có thể đánh giá tính chọn lọc của các từ cụ thể, ngoại trừ những từ phổ biến nhất. Mỗi tài liệu:

join_collapse_limit( integer)

[...]
Vì trình hoạch định truy vấn không phải lúc nào cũng chọn thứ tự tham gia tối ưu, người dùng nâng cao có thể chọn tạm thời đặt biến này thành 1, sau đó chỉ định rõ ràng thứ tự tham gia mà họ mong muốn.

Sử dụng SET LOCALcho mục đích chỉ đặt nó cho giao dịch hiện tại.

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;

Thứ tự của các WHEREđiều kiện luôn luôn không liên quan. Chỉ có thứ tự tham gia là có liên quan ở đây.

Hoặc sử dụng CTE như @jjanes giải thích trong "Tùy chọn 2". cho một hiệu ứng tương tự.

Chỉ mục

Chỉ số cây B

Lấy tất cả các điều kiện ticketsđược sử dụng giống hệt với hầu hết các truy vấn và tạo một chỉ mục một phần trên tickets:

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

Nếu một trong các điều kiện là biến, hãy loại bỏ nó khỏi WHEREđiều kiện và thay vào đó là cột chỉ mục.

Một số khác trên transactions:

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

Cột thứ ba chỉ để cho phép quét chỉ mục.

Ngoài ra, vì bạn có chỉ mục tổng hợp này với hai cột số nguyên trên attachments:

"attachments3" btree (parent, transactionid)

Chỉ mục bổ sung này là một sự lãng phí hoàn toàn , hãy xóa nó:

"attachments1" btree (parent)

Chi tiết:

Chỉ số GIN

Thêm vào transactionidchỉ số GIN của bạn để làm cho nó hiệu quả hơn rất nhiều. Đây có thể là một viên đạn bạc khác , bởi vì nó có khả năng cho phép quét chỉ mục, loại bỏ hoàn toàn các lượt truy cập vào bảng lớn .
Bạn cần các lớp toán tử bổ sung được cung cấp bởi các mô-đun bổ sung btree_gin. Hướng dẫn chi tiết:

"contentindex_idx" gin (transactionid, contentindex)

4 byte từ một integercột không làm cho chỉ mục lớn hơn nhiều. Ngoài ra, may mắn thay cho bạn, các chỉ mục GIN khác với các chỉ mục B-tree ở một khía cạnh quan trọng. Mỗi tài liệu:

Một chỉ mục GIN nhiều màu có thể được sử dụng với các điều kiện truy vấn liên quan đến bất kỳ tập hợp con nào của các cột của chỉ mục . Không giống như B-tree hoặc GiST, hiệu quả tìm kiếm chỉ mục là như nhau cho dù các điều kiện truy vấn sử dụng cột chỉ mục nào.

Nhấn mạnh đậm của tôi. Vì vậy, bạn chỉ cần một chỉ số GIN (lớn và hơi tốn kém).

Bảng định nghĩa

Di chuyển integer not null columnsra phía trước. Điều này có một vài hiệu ứng tích cực nhỏ về lưu trữ và hiệu suất. Lưu 4 - 8 byte mỗi hàng trong trường hợp này.

                      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

lựa chọn 1

Người lập kế hoạch không có cái nhìn sâu sắc về bản chất thực sự của mối quan hệ giữa hiệu quả và id, và vì vậy có lẽ nghĩ rằng mệnh đề:

main.EffectiveId = main.id

sẽ được lựa chọn nhiều hơn so với thực tế. Nếu đây là những gì tôi nghĩ, thìIDID gần như luôn luôn bằng main.id, nhưng người lập kế hoạch không biết điều đó.

Một cách tốt hơn có thể để lưu trữ loại mối quan hệ này thường là xác định giá trị NULL của hiệu quảID có nghĩa là "hiệu quả giống như id" và chỉ lưu trữ một cái gì đó trong đó nếu có sự khác biệt.

Giả sử bạn không muốn tổ chức lại lược đồ của mình, bạn có thể cố gắng khắc phục bằng cách viết lại mệnh đề đó như sau:

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

Người lập kế hoạch có thể cho rằng cái betweenít chọn lọc hơn một đẳng thức, và điều đó có thể đủ để đưa nó ra khỏi cái bẫy hiện tại của nó.

Lựa chọn 2

Một cách tiếp cận khác là sử dụng CTE:

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

Điều này buộc người lập kế hoạch sử dụng Content Index làm nguồn chọn lọc. Một khi nó bị buộc phải làm điều đó, các tương quan cột gây hiểu lầm trên bảng Vé sẽ không còn trông hấp dẫn nữa. Tất nhiên, nếu ai đó tìm kiếm "vấn đề" thay vì "frobnicate", điều đó có thể gây phản tác dụng.

Lựa chọn 3

Để điều tra các ước tính hàng xấu hơn nữa, bạn nên chạy truy vấn bên dưới trong tất cả 2 ^ 3 = 8 hoán vị của các mệnh đề AND khác nhau được nhận xét. Điều này sẽ giúp tìm ra nơi ước tính xấu đến từ đâu.

explain analyze
SELECT * FROM Tickets main WHERE 
   main.Status != 'deleted' AND 
   main.Type = 'ticket' AND 
   main.EffectiveId = main.id;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.