Pencarian teks lengkap lambat karena perkiraan baris yang sangat tidak akurat

10

Query fulltext terhadap database ini (menyimpan tiket RT ( Request Tracker )) tampaknya membutuhkan waktu yang sangat lama untuk dieksekusi. Tabel lampiran (berisi data teks lengkap) sekitar 15GB.

Skema database adalah sebagai berikut, ini sekitar 2 juta baris:

rt4 = # \ d + lampiran
                                                    Tabel "public.attachments"
     Kolom | Ketik | Pengubah | Penyimpanan | Deskripsi
----------------- + ----------------------------- + - -------------------------------------------------- ------ + ---------- + -------------
 id | integer | bukan null default default ('attachments_id_seq' :: regclass) | polos |
 transactionid | integer | bukan nol | polos |
 induk | integer | bukan nol default 0 | polos |
 messageid | karakter bervariasi (160) | | diperpanjang |
 subjek | karakter bervariasi (255) | | diperpanjang |
 nama file | karakter bervariasi (255) | | diperpanjang |
 contenttype | karakter bervariasi (80) | | diperpanjang |
 contentencoding | karakter bervariasi (80) | | diperpanjang |
 konten | teks | | diperpanjang |
 header | teks | | diperpanjang |
 pencipta | integer | bukan nol default 0 | polos |
 dibuat | timestamp tanpa zona waktu | | polos |
 contentindex | tsvector | | diperpanjang |
Indeks:
    "attachments_pkey" KUNCI UTAMA, btree (id)
    "attachments1" btree (parent)
    "attachments2" btree (transactionid)
    "attachments3" btree (parent, transactionid)
    "contentindex_idx" gin (contentindex)
Memiliki OID: tidak

Saya dapat melakukan query pada database sendiri dengan sangat cepat (<1s) dengan permintaan seperti:

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

Namun, ketika RT menjalankan kueri yang seharusnya melakukan pencarian indeks teks lengkap pada tabel yang sama, biasanya dibutuhkan ratusan detik untuk menyelesaikannya. Output analisis kueri adalah sebagai berikut:

Pertanyaan

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 keluaran

                                                                             RENCANA QUERY 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 Agregat (biaya = 51210.60..51210.61 baris = 1 lebar = 4) (waktu aktual = 477778.806..477778.806 baris = 1 loop = 1)
   -> Nested Loop (biaya = 0,00..51210.57 baris = 15 lebar = 4) (waktu aktual = 17943.986..477775.174 baris = 4197 loop = 1)
         -> Nested Loop (biaya = 0,00..40643.08 baris = 6507 lebar = 8) (waktu aktual = 8.526..20610.380 baris = 1714818 loop = 1)
               -> Seq Memindai pada tiket utama (biaya = 0,00..9818,37 baris = 598 lebar = 8) (waktu aktual = 0,008..256,042 baris = 96990 loop = 1)
                     Saring: (((status) :: teks 'dihapus' :: teks) DAN (id = efektifid) DAN ((tipe) :: teks = 'tiket' :: teks))
               -> Pemindaian Indeks menggunakan transaksi1 pada transaksi transaksi_1 (biaya = 0,00..51,36 baris = 15 lebar = 8) (waktu aktual = 0,102..0.202 baris = 18 loop = 96990)
                     Indeks Cond: (((objecttype) :: text = 'RT :: Ticket' :: text) AND (objectid = main.id))
         -> Pemindaian Indeks menggunakan attachments2 pada attachments attachments_2 (biaya = 0,00..1.61 baris = 1 lebar = 4) (waktu aktual = 0.266..0.266 baris = 0 loop = 1714818)
               Indeks Cond: (transactionid = transaction_1.id)
               Saring: (contentindex @@ plainto_tsquery ('frobnicate' :: text))
 Total runtime: 477778.883 ms

Sejauh yang saya tahu, masalahnya tampaknya bukan menggunakan indeks yang dibuat di contentindexlapangan ( contentindex_idx), melainkan melakukan filter pada sejumlah besar baris yang cocok di tabel lampiran. Jumlah baris dalam output yang dijelaskan juga tampak sangat tidak akurat, bahkan setelah yang terbaru ANALYZE: baris yang diestimasi = 6507 baris aktual = 1714818.

Saya tidak begitu yakin ke mana harus pergi dengan ini.

James Hannah
sumber
Peningkatan akan menghasilkan manfaat tambahan. Selain banyak perbaikan umum, khususnya: 9.2 memungkinkan hanya pemindaian indeks dan peningkatan skalabilitas. 9,4 mendatang akan membawa peningkatan besar untuk indeks GIN.
Erwin Brandstetter

Jawaban:

5

Ini dapat ditingkatkan dalam seribu satu cara, maka itu harus dalam hitungan milidetik .

Pertanyaan yang lebih baik

Ini hanya permintaan Anda yang diformat ulang dengan alias dan beberapa derau dihapus untuk menghapus kabut:

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

Sebagian besar masalah dengan kueri Anda terletak pada dua tabel pertama ticketsdan transactions, yang hilang dari pertanyaan. Saya mengisi dengan tebakan yang terpelajar.

  • t.status, t.objecttypedan tr.objecttypemungkin seharusnya tidak text, tetapi enumatau mungkin beberapa nilai yang sangat kecil merujuk tabel pencarian.

EXISTS setengah bergabung

Dengan asumsi tickets.idadalah kunci utama, formulir yang ditulis ulang ini harus jauh lebih murah:

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

Alih-alih mengalikan baris dengan dua 1: n bergabung, hanya untuk menciutkan beberapa pertandingan pada akhirnya dengan count(DISTINCT id), gunakan EXISTSsemi-gabung, yang dapat berhenti mencari lebih jauh begitu pertandingan pertama ditemukan dan pada saat yang sama mengacaukan DISTINCTlangkah terakhir . Per dokumentasi:

Subquery umumnya hanya akan dieksekusi cukup lama untuk menentukan apakah setidaknya satu baris dikembalikan, tidak semua jalan ke penyelesaian.

Efektivitas tergantung pada berapa banyak transaksi per tiket dan lampiran per transaksi yang ada.

Tentukan urutan bergabung dengan join_collapse_limit

Jika Anda tahu bahwa istilah pencarian Anda untuk attachments.contentindexyang sangat selektif - lebih selektif dibandingkan kondisi lain di query (yang mungkin kasus untuk 'frobnicate', tetapi tidak untuk 'masalah'), Anda dapat memaksa urutan bergabung. Perencana kueri tidak dapat menilai selektivitas kata tertentu, kecuali yang paling umum. Per dokumentasi:

join_collapse_limit( integer)

[...]
Karena perencana kueri tidak selalu memilih urutan bergabung yang optimal, pengguna tingkat lanjut dapat memilih untuk sementara mengatur variabel ini menjadi 1, dan kemudian menentukan urutan bergabung yang mereka inginkan secara eksplisit.

Gunakan SET LOCALuntuk tujuan hanya mengaturnya untuk transaksi saat ini.

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;

Urutan WHEREkondisi selalu tidak relevan. Hanya urutan gabungan yang relevan di sini.

Atau gunakan CTE seperti @jjanes menjelaskan dalam "Opsi 2". untuk efek yang serupa.

Indeks

Indeks B-tree

Ambil semua kondisi ticketsyang digunakan secara identik dengan sebagian besar kueri dan buat indeks parsial pada tickets:

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

Jika salah satu kondisi adalah variabel, jatuhkan dari WHEREkondisi dan tambahkan kolom sebagai kolom indeks.

Satu lagi di transactions:

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

Kolom ketiga hanya untuk mengaktifkan pemindaian hanya indeks.

Juga, karena Anda memiliki indeks komposit ini dengan dua kolom bilangan bulat di attachments:

"attachments3" btree (parent, transactionid)

Indeks tambahan ini adalah pemborosan total , hapus:

"attachments1" btree (parent)

Detail:

Indeks GIN

Tambahkan transactionidke indeks GIN Anda untuk membuatnya jauh lebih efektif. Ini mungkin peluru perak lain , karena berpotensi memungkinkan scan indeks saja, menghilangkan kunjungan ke meja besar sepenuhnya.
Anda memerlukan kelas operator tambahan yang disediakan oleh modul tambahan btree_gin. Instruksi terperinci:

"contentindex_idx" gin (transactionid, contentindex)

4 byte dari integerkolom tidak membuat indeks lebih besar. Juga, untungnya bagi Anda, indeks GIN berbeda dari indeks B-tree dalam aspek penting. Per dokumentasi:

Indeks GIN multikolom dapat digunakan dengan kondisi kueri yang melibatkan bagian mana pun dari kolom indeks . Tidak seperti B-tree atau GiST, efektivitas pencarian indeks adalah sama terlepas dari kolom indeks mana yang digunakan kondisi kueri.

Penekanan berani saya. Jadi Anda hanya perlu satu indeks GIN (besar dan agak mahal).

Definisi tabel

Pindahkan integer not null columnske depan. Ini memiliki beberapa efek positif kecil pada penyimpanan dan kinerja. Menghemat 4 - 8 byte per baris dalam hal ini.

                      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                    |
Erwin Brandstetter
sumber
3

Pilihan 1

Perencana tidak memiliki wawasan tentang sifat sebenarnya dari hubungan antara EffectiveId dan id, dan mungkin berpikir klausa:

main.EffectiveId = main.id

akan menjadi jauh lebih selektif daripada yang sebenarnya. Jika ini yang saya pikirkan, EffectiveID hampir selalu sama dengan main.id, tetapi perencana tidak mengetahuinya.

Cara yang mungkin lebih baik untuk menyimpan jenis hubungan ini adalah dengan menentukan nilai NULL dari EffectiveID yang berarti "secara efektif sama dengan id", dan menyimpan sesuatu di dalamnya hanya jika ada perbedaan.

Dengan asumsi Anda tidak ingin mengatur ulang skema Anda, Anda dapat mencoba menyiasatinya dengan menulis ulang klausa itu sebagai sesuatu seperti:

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

Perencana mungkin berasumsi bahwa betweenitu kurang selektif daripada kesetaraan, dan itu mungkin cukup untuk mengeluarkannya dari perangkap saat ini.

pilihan 2

Pendekatan lain adalah dengan menggunakan CTE:

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

Ini memaksa perencana untuk menggunakan ContentIndex sebagai sumber selektivitas. Setelah dipaksa untuk melakukan itu, korelasi kolom yang menyesatkan pada tabel Tiket tidak akan lagi terlihat begitu menarik. Tentu saja jika seseorang mencari 'masalah' daripada 'frobnicate', itu mungkin menjadi bumerang.

Opsi 3

Untuk menyelidiki perkiraan baris buruk lebih lanjut, Anda harus menjalankan kueri di bawah ini dalam semua 2 ^ 3 = 8 permutasi dari berbagai klausa DAN yang dikomentari. Ini akan membantu mencari tahu dari mana perkiraan buruk itu berasal.

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