ค้นหา fulltext ช้าเนื่องจากการประมาณการแถวไม่ถูกต้องอย่างดุร้าย


10

ข้อความค้นหาแบบเต็มกับฐานข้อมูลนี้ (การเก็บตั๋วRT ( Request Tracker )) ดูเหมือนจะใช้เวลานานมากในการดำเนินการ ตารางสิ่งที่แนบ (มีข้อมูลแบบเต็ม) ประมาณ 15GB

สคีมาฐานข้อมูลมีดังนี้ประมาณ 2 ล้านแถว:

rt4 = # \ d + ไฟล์แนบ
                                                    ตาราง "public.attachments"
     คอลัมน์ | ประเภท | ตัวดัดแปลง | จัดเก็บข้อมูล | ลักษณะ
----------------- + ----------------------------- + - -------------------------------------------------- ------ ---------- + + -------------
 id | จำนวนเต็ม ไม่ใช่ null ค่าเริ่มต้น nextval ('Attachments_id_seq' :: regclass) | ธรรมดา
 ธุรกรรม | จำนวนเต็ม ไม่เป็นโมฆะ ธรรมดา
 parent | จำนวนเต็ม ไม่ใช่ค่าเริ่มต้นเป็นศูนย์ 0 | ธรรมดา
 messageid | อักขระแตกต่างกัน (160) | | ขยาย
 หัวเรื่อง | อักขระแตกต่างกัน (255) | | ขยาย
 ชื่อไฟล์ อักขระแตกต่างกัน (255) | | ขยาย
 contenttype | อักขระแตกต่างกัน (80) | | ขยาย
 การเข้ารหัสเนื้อหา | อักขระแตกต่างกัน (80) | | ขยาย
 เนื้อหา | ข้อความ | | ขยาย
 ส่วนหัว ข้อความ | | ขยาย
 ผู้สร้าง | จำนวนเต็ม ไม่ใช่ค่าเริ่มต้นเป็นศูนย์ 0 | ธรรมดา
 สร้าง | การประทับเวลาที่ไม่มีโซนเวลา | | ธรรมดา
 contentindex | tsvector | | ขยาย
ดัชนี:
    "Attachments_pkey" คีย์หลัก, btree (id)
    "Attachments1" btree (parent)
    "เอกสารแนบ 2" btree (transactionid)
    "Attachments3" btree (parent, transactionid)
    "contentindex_idx" gin (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 เอาท์พุต

                                                                             แผน QUERY 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 ผลรวม (ราคา = 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)
               -> Seq สแกนบนตั๋วหลัก (ราคา = 0.00..9818.37 แถว = 598 กว้าง = 8) (เวลาจริง = 0.008 ..256.042 แถว = 96990 ลูป = 1)
                     ตัวกรอง: ((((สถานะ) :: ข้อความ 'ลบ' :: ข้อความ) และ (id = effectiveid) และ ((ประเภท) :: text = 'ตั๋ว' :: ข้อความ))
               -> การสแกนดัชนีโดยใช้ทรานแซคชัน 1 ในทรานแซคชัน transaction_1 (ต้นทุน = 0.00..51.36 แถว = 15 ความกว้าง = 8) (เวลาจริง = 0.102..0.202 แถว = 18 ลูป = 96990)
                     ดัชนี Cond: ((((objecttype)) :: text = 'RT :: Ticket' :: text) AND (objectid = main.id))
         -> ดัชนีการสแกนโดยใช้ไฟล์แนบ 2 ในไฟล์แนบ Attachments_2 (ราคา = 0.00..1.61 แถว = 1 ความกว้าง = 4) (เวลาจริง = 0.266..0.266 แถว = 0 ลูป = 1714818)
               ดัชนี Cond: (transactionid = transaction_1.id)
               ตัวกรอง: (contentindex @@ plainto_tsquery ('frobnicate' :: ข้อความ))
 รันไทม์ทั้งหมด: 477778.883 ms

เท่าที่ฉันสามารถบอกได้ปัญหาดูเหมือนว่ามันไม่ได้ใช้ดัชนีที่สร้างขึ้นในcontentindexฟิลด์ ( contentindex_idx) แต่มันกำลังทำตัวกรองในจำนวนแถวที่ตรงกันจำนวนมากในตารางสิ่งที่แนบมา จำนวนแถวในเอาต์พุตอธิบายยังดูเหมือนว่าไม่ถูกต้องอย่างดุเดือดแม้กระทั่งเมื่อเร็ว ๆ นี้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');

ปัญหาส่วนใหญ่ของแบบสอบถามของคุณอยู่ในสองตารางแรก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')
   );

แทนการคูณแถวด้วยสองตัวรวม 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เงื่อนไขไม่เกี่ยวข้องเสมอ เฉพาะลำดับการรวมเท่านั้นที่เกี่ยวข้องที่นี่

หรือใช้ CTE เช่น @jjanes อธิบายใน "ตัวเลือก 2" สำหรับเอฟเฟกต์ที่คล้ายกัน

ดัชนี

ดัชนีต้นไม้ 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)

รายละเอียด:

ดัชนี GIN

เพิ่มtransactionidในดัชนี GIN ของคุณเพื่อให้มีประสิทธิภาพมากขึ้น นี่อาจเป็นสัญลักษณ์แสดงหัวข้อย่อยสีเงินอีกอันเนื่องจากอาจช่วยให้สแกนเฉพาะดัชนีทำให้ไม่สามารถเข้าชมตารางขนาดใหญ่ได้อย่างสมบูรณ์ คุณต้องเรียนผู้ประกอบการเพิ่มเติมโดยโมดูลเพิ่มเติม
btree_ginคำแนะนำโดยละเอียด:

"contentindex_idx" gin (transactionid, contentindex)

4 ไบต์จากintegerคอลัมน์ไม่ทำให้ดัชนีใหญ่ขึ้น นอกจากนี้โชคดีสำหรับคุณที่ดัชนี GIN นั้นแตกต่างจากดัชนี B-tree ในแง่มุมที่สำคัญ ตามเอกสาร:

ดัชนีหลายคอลัมน์ GIN สามารถใช้กับเงื่อนไขแบบสอบถามที่เกี่ยวข้องกับการ ย่อยของคอลัมน์ดัชนีใดซึ่งแตกต่างจาก B-tree หรือ 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 เกือบตลอดเวลา แต่ผู้วางแผนไม่ทราบ

วิธีที่ดีกว่าในการจัดเก็บความสัมพันธ์ประเภทนี้มักจะกำหนดค่า NULL ของ EffectiveID ให้หมายถึง "มีประสิทธิภาพเหมือนกับ 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 เป็นแหล่งของการเลือก เมื่อถูกบังคับให้ทำเช่นนั้นความสัมพันธ์ของคอลัมน์ที่ทำให้เข้าใจผิดในตารางตั๋วจะไม่น่าดึงดูดอีกต่อไป แน่นอนถ้ามีคนค้นหา 'ปัญหา' มากกว่า 'frobnicate' นั่นอาจย้อนกลับมา

ตัวเลือก 3

หากต้องการตรวจสอบการประมาณการแถวที่ไม่ถูกต้องเพิ่มเติมคุณควรเรียกใช้แบบสอบถามด้านล่างในการเปลี่ยนลำดับทั้งหมด 2 ^ 3 = 8 ของส่วนคำสั่ง AND ที่ต่างกันและแสดงความคิดเห็น สิ่งนี้จะช่วยให้ทราบว่าการประมาณการที่ไม่ดีมาจากไหน

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.