Ricerca full-text lenta a causa di stime di riga estremamente imprecise


10

Le query full-text su questo database (memorizzazione dei ticket RT ( Request Tracker ) sembrano richiedere molto tempo per essere eseguite. La tabella degli allegati (contenente i dati full text) è di circa 15 GB.

Lo schema del database è il seguente, circa 2 milioni di righe:

rt4 = # \ d + allegati
                                                    Tabella "public.attachments"
     Colonna | Digita | Modificatori | Conservazione | Descrizione
----------------- + ----------------------------- + - -------------------------------------------------- ------ + ---------- + -------------
 id | intero | nextval default non nullo ('attachments_id_seq' :: regclass) | pianura |
 transazione | intero | non nullo | pianura |
 genitore | intero | non null default 0 | pianura |
 messageid | carattere variabile (160) | | esteso |
 soggetto | carattere variabile (255) | | esteso |
 nome file | carattere variabile (255) | | esteso |
 contenttype | carattere variabile (80) | | esteso |
 codifica dei contenuti | carattere variabile (80) | | esteso |
 contenuto | testo | | esteso |
 intestazioni | testo | | esteso |
 creatore | intero | non null default 0 | pianura |
 creato | timestamp senza fuso orario | | pianura |
 contentindex | tsvector | | esteso |
indici:
    TASTO PRIMARIO "attachments_pkey", btree (id)
    "attachments1" btree (genitore)
    "attachments2" btree (transactionid)
    "attachments3" btree (padre, transazione)
    "contentindex_idx" gin (contentindex)
Ha OID: no

Posso interrogare il database da solo molto rapidamente (<1s) con una query come:

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

Tuttavia, quando RT esegue una query che dovrebbe eseguire una ricerca dell'indice full-text sulla stessa tabella, il completamento richiede in genere centinaia di secondi. L'output dell'analisi della query è il seguente:

domanda

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 produzione

                                                                             PIANO DI QUERY 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 Aggregato (costo = 51210.60..51210.61 righe = 1 larghezza = 4) (tempo effettivo = 477778.806..477778.806 righe = 1 loop = 1)
   -> Loop nidificato (costo = 0,00..51210,57 righe = 15 larghezza = 4) (tempo effettivo = 17943,986..477775.174 righe = 4197 loop = 1)
         -> Nested Loop (costo = 0.00..40643.08 righe = 6507 larghezza = 8) (tempo effettivo = 8.526..20610.380 righe = 1714818 loop = 1)
               -> Scansione Seq su ticket principale (costo = 0,00..9818,37 righe = 598 larghezza = 8) (tempo effettivo = 0,008..256,042 righe = 96990 loop = 1)
                     Filtro: (((stato) :: testo 'cancellato' :: testo) AND (id = efficace) AND ((tipo) :: testo = 'ticket' :: testo))
               -> Scansione indice utilizzando le transazioni1 sulle transazioni_1 (costo = 0,00..51,36 righe = 15 larghezza = 8) (tempo effettivo = 0,102..0,202 righe = 18 cicli = 96990)
                     Index Cond: ((((objecttype) :: text = 'RT :: Ticket' :: text) AND (objectid = main.id))
         -> Scansione indice utilizzando allegati2 su allegati attachments_2 (costo = 0,00..1,61 righe = 1 larghezza = 4) (tempo effettivo = 0,266..0,266 righe = 0 loop = 1714818)
               Indice cond: (transactionid = transazioni_1.id)
               Filtro: (contentindex @@ plainto_tsquery ('frobnicate' :: text))
 Durata totale: 477778.883 ms

Per quanto posso dire, il problema sembra essere che non sta usando l'indice creato sul contentindexcampo ( contentindex_idx), piuttosto sta facendo un filtro su un gran numero di righe corrispondenti nella tabella degli allegati. Anche i conteggi delle righe nell'output di spiegazione sembrano essere imprecisi, anche dopo un recente ANALYZE: righe stimate = 6507 righe effettive = 1714818.

Non sono davvero sicuro di dove andare dopo con questo.


L'aggiornamento produrrebbe ulteriori vantaggi. Oltre a molti miglioramenti generali, in particolare: 9.2 consente scansioni solo indice e miglioramenti alla scalabilità. Il prossimo 9.4 porterà importanti miglioramenti per gli indici GIN.
Erwin Brandstetter,

Risposte:


5

Questo può essere migliorato in mille modi, quindi dovrebbe essere una questione di millisecondi .

Domande migliori

Questa è solo la tua query riformattata con alias e un po 'di rumore rimosso per eliminare la nebbia:

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

Gran parte del problema con la tua query risiede nelle prime due tabelle ticketse transactions, che mancano alla domanda. Sto compilando con ipotesi colte.

  • t.status, t.objecttypee tr.objecttypeprobabilmente non dovrebbe esserlo text, ma enumo forse un valore molto piccolo che fa riferimento a una tabella di ricerca.

EXISTS semi-join

Supponendo che tickets.idsia la chiave primaria, questa forma riscritta dovrebbe essere molto più economica:

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

Invece di moltiplicare le righe con due join 1: n, solo per comprimere più corrispondenze alla fine con count(DISTINCT id), utilizzare un EXISTSsemi-join, che può smettere di guardare oltre non appena viene trovata la prima corrispondenza e allo stesso tempo oscura il DISTINCTpassaggio finale . Per documentazione:

La subquery verrà generalmente eseguita solo abbastanza a lungo per determinare se viene restituita almeno una riga, non fino al completamento.

L'efficacia dipende da quante transazioni ci sono per biglietto e allegati per transazione.

Determinare l'ordine dei join con join_collapse_limit

Se sai che il termine di ricerca attachments.contentindexè molto selettivo , più selettivo di altre condizioni nella query (che è probabilmente il caso di "frobnicate", ma non di "problema"), puoi forzare la sequenza di join. Il planner delle query difficilmente può giudicare la selettività di parole particolari, ad eccezione di quelle più comuni. Per documentazione:

join_collapse_limit( integer)

[...]
Poiché il pianificatore di query non sceglie sempre l'ordine di join ottimale, gli utenti esperti possono scegliere di impostare temporaneamente questa variabile su 1, quindi specificare l'ordine di join desiderato in modo esplicito.

Utilizzare SET LOCALallo scopo di impostarlo solo per la transazione corrente.

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;

L'ordine delle WHEREcondizioni è sempre irrilevante. Solo l'ordine dei join è rilevante qui.

Oppure usa un CTE come spiega @jjanes in "Opzione 2". per un effetto simile.

indici

Indici B-tree

Accetta tutte le condizioni ticketsutilizzate in modo identico con la maggior parte delle query e crea un indice parziale su tickets:

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

Se una delle condizioni è variabile, eliminala dalla WHEREcondizione e antepone invece la colonna come colonna indice.

Un altro su transactions:

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

La terza colonna è solo per abilitare le scansioni solo indice.

Inoltre, poiché hai questo indice composito con due colonne intere su attachments:

"attachments3" btree (parent, transactionid)

Questo indice aggiuntivo è uno spreco completo , eliminalo:

"attachments1" btree (parent)

Dettagli:

Indice GIN

Aggiungi transactionidal tuo indice GIN per renderlo molto più efficace. Questo potrebbe essere un altro proiettile d'argento , perché potenzialmente consente scansioni solo indice, eliminando completamente le visite al tavolo grande .
Sono necessarie ulteriori classi di operatori fornite dal modulo aggiuntivo btree_gin. Istruzioni dettagliate:

"contentindex_idx" gin (transactionid, contentindex)

4 byte da una integercolonna non rendono l'indice molto più grande. Inoltre, fortunatamente per te, gli indici GIN sono diversi dagli indici B-tree in un aspetto cruciale. Per documentazione:

Un indice GIN a più colonne può essere utilizzato con condizioni di query che coinvolgono qualsiasi sottoinsieme delle colonne dell'indice . A differenza di B-tree o GiST, l' efficacia della ricerca dell'indice è la stessa indipendentemente dalle colonne dell'indice utilizzate dalle condizioni della query.

Enorme enfasi sulla mia. Quindi, è sufficiente l' uno indice (grande e un po 'costosa) GIN.

Definizione della tabella

Sposta integer not null columnsin avanti. Ciò ha un paio di effetti positivi minori su archiviazione e prestazioni. In questo caso, salva 4 - 8 byte per riga.

                      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

opzione 1

Il pianificatore non ha alcuna comprensione della vera natura della relazione tra EffectiveId e id, e quindi probabilmente pensa alla clausola:

main.EffectiveId = main.id

sarà molto più selettivo di quanto non sia in realtà. Se questo è quello che penso, EffectiveID è quasi sempre uguale a main.id, ma il pianificatore non lo sa.

Un modo forse migliore per archiviare questo tipo di relazione è di solito definire il valore NULL di EffectiveID per indicare "effettivamente lo stesso ID" e archiviare qualcosa in esso solo se c'è una differenza.

Supponendo che non desideri riorganizzare il tuo schema, puoi provare a aggirarlo riscrivendo quella clausola come qualcosa del tipo:

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

Il pianificatore potrebbe presumere che betweensia meno selettivo di un'uguaglianza, e ciò potrebbe essere sufficiente per eliminarlo dalla sua attuale trappola.

opzione 2

Un altro approccio è utilizzare un CTE:

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

Ciò impone al pianificatore di utilizzare ContentIndex come fonte di selettività. Una volta costretto a farlo, le correlazioni delle colonne fuorvianti nella tabella dei biglietti non saranno più così attraenti. Ovviamente se qualcuno cerca il "problema" piuttosto che il "frobnicato", ciò potrebbe ritorcersi contro.

Opzione 3

Per analizzare ulteriormente le stime delle righe non valide, è necessario eseguire la query seguente in tutte le permutazioni 2 ^ 3 = 8 delle diverse clausole AND commentate. Questo aiuterà a capire da dove proviene la cattiva stima.

explain analyze
SELECT * FROM Tickets main WHERE 
   main.Status != 'deleted' AND 
   main.Type = 'ticket' AND 
   main.EffectiveId = main.id;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.