Çılgınca yanlış satır tahminleri nedeniyle yavaş tam metin araması


10

Bu veritabanına (RT ( Request Tracker ) biletlerini saklamak) yönelik tam metin sorgularının yürütülmesi çok uzun zaman alıyor gibi görünüyor. Ekler tablosu (tam metin verilerini içeren) yaklaşık 15 GB'dir.

Veritabanı şeması aşağıdaki gibidir, yaklaşık 2 milyon satır:

rt4 = # \ d + ekler
                                                    "Public.attachments" tablosu
     Sütun | Türü | Değiştiriciler | Depolama | Açıklama
----------------- + ----------------------------- + - -------------------------------------------------- ------ + ---------- + -------------
 id | tam sayı | null değil varsayılan nextval ('attachments_id_seq' :: regclass) | düz |
 işlem kimliği | tam sayı | boş değil | düz |
 ebeveyn | tam sayı | boş değil varsayılan 0 | düz |
 mesaj kimliği | karakter değiştirme (160) | | genişletilmiş |
 konu | karakter değiştirme (255) | | genişletilmiş |
 dosyaadı | karakter değiştirme (255) | | genişletilmiş |
 içerik türü | karakter değiştirme (80) | | genişletilmiş |
 içerik kodlama | karakter değiştirme (80) | | genişletilmiş |
 içerik | metin | | genişletilmiş |
 başlıklar | metin | | genişletilmiş |
 yaratıcısı | tam sayı | boş değil varsayılan 0 | düz |
 oluşturuldu | saat dilimi olmadan zaman damgası | | düz |
 içerik dizini | tsvector | | genişletilmiş |
Endeksler:
    "attachments_pkey" BİRİNCİL ANAHTAR, btree (id)
    "attachments1" btree (üst öğe)
    "attachments2" btree (işlem kimliği)
    "attachments3" btree (üst öğe, işlem kimliği)
    "contentindex_idx" cin (contentindex)
OID var: hayır

Ben çok hızlı bir şekilde kendi veritabanı (<1s) gibi bir sorgu ile sorgulayabilirsiniz:

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

Ancak, RT aynı tabloda tam metin dizin araması gerçekleştirmesi gereken bir sorgu çalıştırdığında, tamamlanması genellikle yüzlerce saniye sürer. Sorgu analizi çıktısı aşağıdaki gibidir:

Sorgu

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 çıktı

                                                                             QUERY PLAN 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 Toplam (maliyet = 51210.60..51210.61 satır = 1 genişlik = 4) (gerçek zaman = 477778.806..477778.806 satır = 1 döngü = 1)
   -> İç İçe Döngü (maliyet = 0.00..51210.57 satır = 15 genişlik = 4) (gerçek zaman = 17943.986..477775.174 satır = 4197 döngü = 1)
         -> İç İçe Döngü (maliyet = 0.00..40643.08 satır = 6507 genişlik = 8) (gerçek zaman = 8.526..20610.380 satır = 1714818 döngü = 1)
               -> Seq Scan biletlerinde ana (maliyet = 0.00..9818.37 satır = 598 genişlik = 8) (gerçek zaman = 0.008..256.042 satır = 96990 döngü = 1)
                     Filtre: (((durum) :: metin 'silindi' :: metin) AND (id = etkiliid) AND ((tür) :: metin = 'bilet' :: metin))
               -> İşlemler işlemlerinde işlem1'i kullanarak Dizin Taraması_1 (maliyet = 0.00..51.36 satır = 15 genişlik = 8) (gerçek zaman = 0.102..0.202 satır = 18 döngü = 96990)
                     Endeks Koşulu: (((nesne türü) :: text = 'RT :: Bilet' :: metin) AND (objectid = main.id))
         -> Ekler_2 (maliyet = 0.00..1.61 satır = 1 genişlik = 4) eklerde2 ekleri kullanarak Dizin Taraması (gerçek zaman = 0.266..0.266 satır = 0 döngü = 1714818)
               Endeks Koşulu: (transactionid = transaction_1.id)
               Filtre: (contentindex @@ plainto_tsquery ('frobnicate' :: metin))
 Toplam çalışma süresi: 477778.883 ms

Anlayabildiğim kadarıyla, sorun, contentindexalanda oluşturulan dizini kullanmıyor ( contentindex_idx), bunun yerine ekler tablosundaki çok sayıda eşleşen satırda bir filtre yapıyor olması gibi görünüyor. Açıklama çıktısındaki satır sayıları da yakın ANALYZEzamandan sonra bile çok yanlış görünüyor : tahmini satırlar = 6507 gerçek satırlar = 1714818.

Bundan sonra nereye gideceğimi pek bilmiyorum.


Yükseltme ek faydalar sağlayacaktır. Birçok genel iyileştirmenin yanı sıra , özellikle: 9.2 yalnızca dizin taramasına ve ölçeklenebilirlikte iyileştirmelere olanak tanır. Yaklaşan 9.4, GIN endeksleri için önemli geliştirmeler getirecek.
Erwin Brandstetter

Yanıtlar:


5

Bu bin bir şekilde geliştirilebilir, o zaman milisaniye meselesi olmalı .

Daha İyi Sorgular

Bu yalnızca takma adlarla yeniden biçimlendirilmiş sorgunuz ve sisi temizlemek için biraz gürültü kaldırıldı:

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

Sorgunuzla ilgili sorunun çoğu , ilk iki tabloda yer almaktadır ticketsve transactionsbunlar soruda yoktur. Eğitimli tahminlerle dolduruyorum.

  • t.status, t.objecttypeVe tr.objecttypemuhtemelen olmamalı text, ama enumya bir taramalı tabloya başvuran muhtemelen çok küçük bazı değer.

EXISTS Yarı birleştirme

tickets.idBirincil anahtar olduğunu varsayarak , bu yeniden yazılan form çok daha ucuz olmalıdır:

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

Bunun yerine iki 1'den satırları çarpımının: n katılır, yalnızca sonunda birden fazla eşleşme daraltmak için count(DISTINCT id)kullanmak, bir EXISTSyarı katılmak ilk eşleşme bulunursa gibi başka en kısa sürede seyir durdurmak ve aynı zamanda nihai obsoletes hangi, DISTINCTadım. Belgelere göre:

Alt sorgu genellikle en az bir satırın döndürülüp döndürülmeyeceğini belirlemek için yeterince uzun yürütülür, tamamlamanın sonuna kadar değil.

Etkinlik, bilet başına kaç işlem ve işlem başına ek sayısı olduğuna bağlıdır.

İle birleşme sırasını belirleme join_collapse_limit

Eğer varsa bilmek için arama terimi o attachments.contentindexolduğunu çok seçici - (muhtemelen değil 'sorun' için 'frobnicate' için böyledir) sorgudaki diğer koşullara göre daha seçici arayarak katılan sırasını zorlayabilir. Sorgu planlayıcısı, en sık kullanılanlar hariç, belirli kelimelerin seçiciliğini zorlukla değerlendirebilir. Belgelere göre:

join_collapse_limit( integer)

[...]
Sorgu planlayıcısı her zaman en uygun birleştirme sırasını seçmediğinden, ileri düzey kullanıcılar bu değişkeni geçici olarak 1'e ayarlamayı seçebilir ve sonra açıkça istedikleri birleştirme sırasını belirleyebilir.

SET LOCALYalnızca geçerli işlem için ayarlamak amacıyla kullanın .

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;

Sırası WHEREkoşulları olduğu zaman ilgisiz. Burada yalnızca birleşme sırası geçerlidir.

Ya da "Seçenek 2" de açıklandığı gibi @jjanes gibi bir CTE kullanın . benzer bir etki için.

endeksleri

B-ağacı dizinleri

ticketsÇoğu sorguyla aynı şekilde kullanılan tüm koşulları alın ve aşağıdakiler üzerinde kısmi bir dizin oluşturun tickets:

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

Koşullardan biri değişkense, WHEREkoşuldan çıkarın ve sütunu dizin sütunu olarak yerleştirin.

Başka bir tane transactions:

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

Üçüncü sütun sadece dizin taramayı etkinleştirmektir.

Ayrıca, üzerinde iki tamsayı sütunu olan bu bileşik dizine sahip olduğunuz için attachments:

"attachments3" btree (parent, transactionid)

Bu ek indeks tam bir israftır , silin:

"attachments1" btree (parent)

Detaylar:

GIN endeksi

transactionidDaha etkili hale getirmek için GIN dizininize ekleyin . Bu başka bir gümüş mermi olabilir , çünkü potansiyel olarak sadece dizin taramasına izin verir ve büyük masaya ziyaretleri tamamen ortadan kaldırır .
Ek modül tarafından sağlanan ek operatör sınıflarına ihtiyacınız vardır btree_gin. Detaylı talimatlar:

"contentindex_idx" gin (transactionid, contentindex)

Bir integersütundan 4 bayt , dizini daha büyük yapmaz. Ayrıca, neyse ki sizin için, GIN indeksleri çok önemli bir açıdan B-ağacı indekslerinden farklıdır. Belgelere göre:

Çok sütunlu bir GIN dizini , dizinin sütunlarının herhangi bir alt kümesini içeren sorgu koşullarıyla kullanılabilir . B-ağacı veya GiST'den farklı olarak, dizin arama etkinliği, sorgu koşullarının hangi dizin sütun (lar) ını kullanırsa kullansın aynıdır .

Cesur vurgu benim. Yani sadece bir (büyük ve biraz pahalı) GIN endeksine ihtiyacınız var.

Tablo tanımı

integer not null columnsÖne doğru hareket ettirin . Bunun depolama ve performans üzerinde birkaç küçük olumlu etkisi vardır. Bu durumda satır başına 4 - 8 bayt kaydeder.

                      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

seçenek 1

Planlayıcının EffectiveId ve id arasındaki ilişkinin gerçek doğası hakkında hiçbir bilgisi yoktur ve bu nedenle muhtemelen cümle olduğunu düşünür:

main.EffectiveId = main.id

gerçekte olduğundan çok daha seçici olacak. Eğer bence budur, EffectiveID neredeyse her zaman main.id'e eşittir, ancak planlayıcı bunu bilmez.

Bu tür bir ilişkiyi depolamanın daha iyi bir yolu, genellikle "etkin olarak id ile aynı" anlamına gelen EffectiveID'nin NULL değerini tanımlamak ve içinde bir fark olması durumunda bir şeyi saklamaktır.

Şemanızı yeniden düzenlemek istemediğinizi varsayarsak, bu maddeyi aşağıdaki gibi yeniden yazarak dolaşmaya çalışabilirsiniz:

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

Planlamacı, betweeneşitlikten daha az seçici olduğunu varsayabilir ve bu onu mevcut tuzağından çıkarmak için yeterli olabilir.

seçenek 2

Diğer bir yaklaşım CTE kullanmaktır:

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

Bu, planlayıcıyı ContentIndex'i bir seçicilik kaynağı olarak kullanmaya zorlar. Bunu yapmak zorunda kaldığında, Biletler tablosundaki yanıltıcı sütun korelasyonları artık çok çekici görünmeyecek. Tabii eğer birisi 'frobnicate' yerine 'problem' ararsa, bu geri tepebilir.

Seçenek 3

Hatalı satır tahminlerini daha fazla araştırmak için, yorumlanan farklı AND deyimlerinin tüm 2 ^ 3 = 8 permütasyonlarında aşağıdaki sorguyu çalıştırmalısınız. Bu, kötü tahminin nereden geldiğini anlamaya yardımcı olacaktır.

explain analyze
SELECT * FROM Tickets main WHERE 
   main.Status != 'deleted' AND 
   main.Type = 'ticket' AND 
   main.EffectiveId = main.id;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.