Eu tenho um aplicativo chamado 'Links' onde 1) os usuários se reúnem em grupos e adicionam outros, 2) publicam conteúdo uns para os outros nos referidos grupos. Os grupos são definidos por uma links_group
tabela no meu banco de dados postgresql 9.6.5, enquanto as respostas que eles postam neles são definidas por uma links_reply
tabela. No geral, o desempenho do DB é ótimo.
No entanto, uma SELECT
consulta na links_reply
tabela está aparecendo consistentemente em slow_log. Está demorando mais de 500 ms e é ~ 10 vezes mais lento do que estou experimentando na maioria das outras operações do postgresql.
Eu usei o Django ORM para gerar a consulta. Aqui está a chamada ORM: replies = Reply.objects.select_related('writer__userprofile').filter(which_group=group).order_by('-submitted_on')[:25]
. Essencialmente, isso é selecionar as últimas 25 respostas para um determinado objeto de grupo. Também está selecionando associados user
e userprofile
objetos.
Aqui está um exemplo do SQL correspondente do meu log lento: LOG: duration: 8476.309 ms statement:
SELECT
"links_reply"."id", "links_reply"."text",
"links_reply"."which_group_id", "links_reply"."writer_id",
"links_reply"."submitted_on", "links_reply"."image",
"links_reply"."device", "links_reply"."category",
"auth_user"."id", "auth_user"."username",
"links_userprofile"."id", "links_userprofile"."user_id",
"links_userprofile"."score", "links_userprofile"."avatar"
FROM
"links_reply"
INNER JOIN "auth_user"
ON ("links_reply"."writer_id" = "auth_user"."id")
LEFT OUTER JOIN "links_userprofile"
ON ("auth_user"."id" = "links_userprofile"."user_id")
WHERE "links_reply"."which_group_id" = 124479
ORDER BY "links_reply"."submitted_on" DESC
LIMIT 25
Veja os resultados da análise explicativa aqui: https://explain.depesz.com/s/G4X A varredura do índice (para trás) parece estar consumindo o tempo todo.
Aqui está a saída de \d links_reply
:
Table "public.links_reply"
Column | Type | Modifiers
----------------+--------------------------+----------------------------------------------------------
id | integer | not null default nextval('links_reply_id_seq'::regclass)
text | text | not null
which_group_id | integer | not null
writer_id | integer | not null
submitted_on | timestamp with time zone | not null
image | character varying(100) |
category | character varying(15) | not null
device | character varying(10) | default '1'::character varying
Indexes:
"links_reply_pkey" PRIMARY KEY, btree (id)
"category_index" btree (category)
"links_reply_submitted_on" btree (submitted_on)
"links_reply_which_group_id" btree (which_group_id)
"links_reply_writer_id" btree (writer_id)
"text_index" btree (text)
Foreign-key constraints:
"links_reply_which_group_id_fkey" FOREIGN KEY (which_group_id) REFERENCES links_group(id) DEFERRABLE INITIALLY DEFERRED
"links_reply_writer_id_fkey" FOREIGN KEY (writer_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED
Referenced by:
TABLE "links_groupseen" CONSTRAINT "links_groupseen_which_reply_id_fkey" FOREIGN KEY (which_reply_id) REFERENCES links_reply(id) DEFERRABLE INITIALLY DEFERRED
TABLE "links_report" CONSTRAINT "links_report_which_reply_id_fkey" FOREIGN KEY (which_reply_id) REFERENCES links_reply(id) DEFERRABLE INITIALLY DEFERRED
É uma tabela grande (~ 25 milhões de linhas). O hardware em que está operando tem 16 núcleos e 60 GB de memória. Ele compartilha esta máquina com um aplicativo python. Mas tenho monitorado o desempenho do servidor e não vejo gargalos ali.
Existe alguma maneira de melhorar o desempenho desta consulta? Por favor, informe sobre todas as opções (se houver) que tenho aqui.
Observe que essa consulta teve um desempenho excepcionalmente bom até a semana passada . O que mudou desde então? Realizei um pg_dump
e depois pg_restore
do DB (em uma VM separada), e atualizei do Postgresql 9.3.10 para 9.6.5. Também usei um pool de conexões chamado pgbouncer
antes, que ainda não configurei na nova VM para a qual migrei. É isso.
Por fim, também notei (pela experiência do usuário) que para todos os objetos de grupo criados até a semana passada, a consulta ainda é executada rapidamente. Mas todos os novos objetos que estão sendo criados agora estão produzindo um log lento. Isso poderia ser algum tipo de problema de indexação, especificamente com o links_reply_submitted_on
índice?
Atualização: As otimizações prescritas realmente mudaram as coisas. Dar uma olhada:
Principais problemas suspeitos (sinopse)
Você precisa executar
ANALYZE
após uma atualização de versão principal com opg_upgrade
. As estatísticas da tabela não são copiadas. Possivelmente, ajuste as configurações de autovacuum também.Um índice de várias colunas
(which_group_id, submitted_on DESC)
deve servir muito melhor a essa consulta.Consulta
Consulta formatada sem ruído e com aliases de tabela para melhor legibilidade:
Não vejo problemas com a consulta em si.
Corrupção do índice?
(Eu não acho.)
Se você suspeitar de corrupção, execute
REINDEX
. O manual sugere:No caso de acesso concorrente: o bloqueio difere de descartar e recriar índices do zero em vários aspectos. O manual:
Se isso ainda for um problema para operações simultâneas, considere
CREATE INDEX CONCURRENTLY
criar novos índices duplicados e, em seguida, descarte os antigos em uma transação separada.Estatísticas da tabela
No entanto , parece que as estatísticas da tabela são o problema real. Cotação do seu plano de consulta:
Minha ênfase em negrito. Parece que o Postgres baseia esse plano de consulta em estatísticas enganosas. Ele espera muito mais acertos e provavelmente também subestima a seletividade do predicado
which_group_id = 119287
. Acaba filtrando 1,7 milhão de linhas. Isso cheira a estatísticas de tabela imprecisas. E há uma explicação provável, também:Ao atualizar as versões principais
pg_upgrade
, não copia as estatísticas existentes para a nova versão do banco de dados. Recomenda-se executarVACUUM ANALYZE
ou pelo menosANALYZE
depoispg_upgrade
. A ferramenta ainda solicita para lembrá-lo. O manual:Se você não fizer isso, as tabelas ficarão sem estatísticas atuais até que o autovacuum seja acionado por gravações suficientes na tabela (ou algum outro comando utilitário como
CREATE INDEX
ouALTER TABLE
atualizar algumas estatísticas em tempo real).O mesmo vale para qualquer ciclo de despejo/restauração (com
pg_dump
&pg_restore
no seu caso). As estatísticas da tabela não são incluídas no dump.Sua mesa é muito grande
(~25M rows)
. A configuração padrão para autovacuum define o limite como porcentagem do row_count mais o deslocamento fixo. Às vezes, isso não funciona bem para tabelas grandes, levará algum tempo até a próxima análise automática.Execute
ANALYZE
o manual na mesa ou em todo o banco de dados.Relacionado:
Melhor índice
Sim, isso também. O índice
"links_reply_submitted_on" btree (submitted_on)
não está otimizado para o padrão em sua consulta:Como vimos no plano de consulta acima, o Postgres usa uma varredura de índice, lendo o índice de baixo para cima e filtra as não correspondências. Essa abordagem pode ser razoavelmente rápida se todos (poucos!) selecionados
which_group_id
tiverem 25 linhas no passado recente. Mas não funcionará tão bem para uma distribuição desigual de linhas ou muitos valores distintos parawhich_group_id
.Este índice de várias colunas é um ajuste melhor:
Agora, o Postgres pode apenas escolher as 25 primeiras linhas para o selecionado
which_group_id
, independentemente da distribuição de dados.Relacionado:
Mais explicações
Sobre sua observação:
Por quê? Novos objetos podem ainda não ter 25 entradas, então o Postgres precisa continuar varrendo todo o grande índice na esperança de encontrar mais. Embora isso seja extremamente caro com seu antigo índice e plano de consulta, o mesmo é muito barato com o novo índice (e estatísticas de tabela atualizadas).
Além disso, com estatísticas de tabela precisas, o Postgres provavelmente teria usado seu outro índice
"links_reply_which_group_id" btree (which_group_id)
para pegar as poucas linhas existentes rapidamente (e classificar se houver mais de 25). Mas meu novo índice oferece um plano de consulta mais confiável em qualquer caso.Coisas menores
Existem várias outras coisas (menores) que você pode fazer, como otimizar o layout da mesa ou ajustar as configurações de autovacuum, mas essa resposta já é longa o suficiente. Relacionado:
E mais tarde você comentou:
Certamente ajuda a recuperar apenas as colunas que você realmente precisa. Faça isso, além disso. Mas não é a questão principal aqui.