As consultas de texto completo nesse banco de dados (armazenando tíquetes RT ( Request Tracker )) parecem levar muito tempo para serem executadas. A tabela de anexos (contendo os dados de texto completo) tem cerca de 15 GB.
O esquema do banco de dados é o seguinte, são cerca de 2 milhões de linhas:
rt4 = # \ d + anexos Tabela "public.attachments" Coluna | Tipo | Modificadores | Armazenamento | Descrição ----------------- + ----------------------------- + - -------------------------------------------------- ------ + ---------- + ------------- id | inteiro not null default nextval ('attachments_id_seq' :: regclass) | planície | transactionid | inteiro não nulo | planície | pai | inteiro não nulo padrão 0 | planície | messageid | caracteres que variam (160) | | estendido | assunto | caracteres que variam (255) | | estendido | filename | caracteres que variam (255) | | estendido | tipo de conteúdo | caracteres variados (80) | | estendido | codificação de conteúdo | caracteres variados (80) | | estendido | conteúdo | texto | estendido | cabeçalhos | texto | estendido | criador | inteiro não nulo padrão 0 | planície | criado | registro de data e hora sem fuso horário | | planície | índice de conteúdo | tsvector | | estendido | Índices: "attachments_pkey" CHAVE PRIMÁRIA, btree (id) btree "attachments1" (pai) "attachments2" btree (transactionid) btree "attachments3" (pai, ID da transação) gin "contentindex_idx" (contentindex) Possui OIDs: não
Posso consultar o banco de dados por conta própria muito rapidamente (<1s) com uma consulta como:
select objectid
from attachments
join transactions on attachments.transactionid = transactions.id
where contentindex @@ to_tsquery('frobnicate');
No entanto, quando o RT executa uma consulta que deveria executar uma pesquisa de índice de texto completo na mesma tabela, geralmente leva centenas de segundos para concluir. A saída de análise da consulta é a seguinte:
Inquerir
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
resultado
PLANO DE CONSULTA -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------- Agregado (custo = 51210.60..51210.61 linhas = 1 largura = 4) (tempo real = 477778.806..477778.806 linhas = 1 loops = 1) -> Loop aninhado (custo = 0,00..51210,57 linhas = 15 largura = 4) (tempo real = 17943,986..477775.174 linhas = 4197 loops = 1) -> Loop aninhado (custo = 0,00..40643,08 linhas = 6507 largura = 8) (tempo real = 8,526..20610,380 linhas = 1714818 loops = 1) -> Varredura Seq nos tickets principais (custo = 0,00..9818,37 linhas = 598 largura = 8) (tempo real = 0,008..256,042 linhas = 96990 loops = 1) Filtro: (((status) :: texto 'excluído' :: texto) AND (id = eficazid) AND ((tipo) :: texto = 'ticket' :: texto)) -> Varredura de índice usando transações1 em transações transações_1 (custo = 0,00..51,36 linhas = 15 largura = 8) (tempo real = 0,102..0,202 linhas = 18 loops = 96990) Índice Cond: (((objecttype) :: text = 'RT :: Ticket' :: text) AND (objectid = main.id)) -> Varredura de índice usando anexos2 em anexos anexos_2 (custo = 0,00..1,61 linhas = 1 largura = 4) (tempo real = 0,266..0,266 linhas = 0 loops = 1714818) Cond do índice: (transactionid = transaction_1.id) Filtro: (contentindex @@ plainto_tsquery ('frobnicate' :: texto)) Tempo de execução total: 477778.883 ms
Pelo que sei, o problema parece ser que ele não está usando o índice criado no contentindex
campo ( contentindex_idx
), mas está fazendo um filtro em um grande número de linhas correspondentes na tabela de anexos. As contagens de linhas na saída de explicação também parecem ser muito imprecisas, mesmo depois de uma recente ANALYZE
: linhas estimadas = 6507 linhas reais = 1714818.
Não tenho muita certeza de onde ir com isso.
Respostas:
Isso pode ser aprimorado de mil e uma maneiras, então deve ser uma questão de milissegundos .
Melhores consultas
Esta é apenas sua consulta reformatada com aliases e algum ruído removido para limpar o nevoeiro:
A maior parte do problema com sua consulta está nas duas primeiras tabelas
tickets
etransactions
, que estão faltando na pergunta. Estou preenchendo com palpites.t.status
,t.objecttype
etr.objecttype
provavelmente não deveria sertext
, masenum
ou possivelmente algum valor muito pequeno fazendo referência a uma tabela de consulta.EXISTS
semi-junçãoAssumindo que
tickets.id
é a chave primária, esse formulário reescrito deve ser muito mais barato:Em vez de multiplicar linhas com duas junções 1: n, apenas para recolher várias correspondências no final
count(DISTINCT id)
, use umaEXISTS
semi-junção, que pode parar de procurar mais assim que a primeira correspondência for encontrada e ao mesmo tempo obsoleta aDISTINCT
etapa final . Por documentação:A eficácia depende de quantas transações por ticket e anexos por transação existem.
Determinar a ordem das junções com
join_collapse_limit
Se você souber que seu termo de pesquisa
attachments.contentindex
é muito seletivo - mais seletivo do que outras condições da consulta (que provavelmente é o caso de 'frobnicate', mas não de 'problem'), você pode forçar a sequência de junções. O planejador de consultas dificilmente pode julgar a seletividade de palavras específicas, exceto as mais comuns. Por documentação:Use
SET LOCAL
com a finalidade de configurá-lo apenas para a transação atual.A ordem das
WHERE
condições é sempre irrelevante. Somente a ordem das junções é relevante aqui.Ou use um CTE como o @jjanes explica na "Opção 2". para um efeito semelhante.
Índices
Índices da árvore B
Pegue todas as condições
tickets
usadas de maneira idêntica à maioria das consultas e crie um índice parcial emtickets
:Se uma das condições for variável, elimine-a da
WHERE
condição e prefira a coluna como coluna de índice.Outro sobre
transactions
:A terceira coluna é apenas para habilitar verificações apenas de índice.
Além disso, como você tem esse índice composto com duas colunas inteiras em
attachments
:Este índice adicional é um desperdício completo , exclua-o:
Detalhes:
Índice GIN
Adicione
transactionid
ao seu índice GIN para torná-lo muito mais eficaz. Essa pode ser outra vantagem , pois potencialmente permite verificações apenas de índice, eliminando completamente as visitas à grande mesa.Você precisa de classes de operador adicionais fornecidas pelo módulo adicional
btree_gin
. Instruções detalhadas:4 bytes de uma
integer
coluna não aumentam o índice. Felizmente para você, os índices GIN são diferentes dos índices da árvore B em um aspecto crucial. Por documentação:Negrito ênfase minha. Então você só precisa do um índice de GIN (grande e um pouco caro).
Definição de tabela
Mova o
integer not null columns
para a frente. Isso tem alguns efeitos positivos menores no armazenamento e no desempenho. Salva de 4 a 8 bytes por linha neste caso.fonte
Opção 1
O planejador não tem conhecimento da verdadeira natureza do relacionamento entre EffectiveId e id e, portanto, provavelmente pensa na cláusula the:
vai ser muito mais seletivo do que realmente é. Se é isso que eu acho que é, o EffectiveID quase sempre é igual ao main.id, mas o planejador não sabe disso.
Uma maneira possivelmente melhor para armazenar esse tipo de relacionamento é geralmente definir o valor NULL de EffectiveID para significar "efetivamente o mesmo que id" e armazenar algo apenas se houver uma diferença.
Supondo que você não deseja reorganizar seu esquema, tente contorná-lo reescrevendo essa cláusula como algo como:
O planejador pode assumir que
between
é menos seletivo que uma igualdade, e isso pode ser suficiente para desviá-lo de sua armadilha atual.opção 2
Outra abordagem é usar um CTE:
Isso força o planejador a usar o ContentIndex como uma fonte de seletividade. Uma vez forçado a fazer isso, as correlações enganosas da coluna na tabela Tickets não parecerão mais tão atraentes. É claro que se alguém procurar por 'problema' em vez de 'frobnicizar', isso pode sair pela culatra.
Opção 3
Para investigar mais as estimativas de linhas incorretas, você deve executar a consulta abaixo em todas as permutações 2 ^ 3 = 8 das diferentes cláusulas AND comentadas. Isso ajudará a descobrir de onde vem a estimativa ruim.
fonte