Índices corrompidos no PG12, como corrigir?

Estou apenas especulando, mas aqui vai minha teoria maluca:

Executei o Reindex duas vezes simultaneamente (primeiro ccnew, depois ccnew1)
e ambas as vezes apresentou erros.

Talvez, quando falha, ele simplesmente não reverta, deixando o lixo e encerrando.

Isso parece muito, muito, muito provável.

Apenas adicionando minha opinião aqui,

Índices Toast não podem ser removidos dessa forma.
Fui obrigado a:

su postgres
psql
\connect discourse
drop index pg_toast.pg_toast_309322_index_ccnew;
drop index pg_toast.pg_toast_309322_index_ccnew1;

O acima se aplica apenas ao pg toast, pois o usuário do Discourse não tem acesso a esse índice.
PG::InsufficientPrivilege: ERROR: permission denied for schema pg_toast

remover índices de toast é super assustador. Acho que você não tem escolha aqui, não é? Sim, você precisa fazer isso pelo psql.

e aqui vem o reindex feliz:

discourse=# REINDEX SCHEMA CONCURRENTLY public;
REINDEX

:partying_face:

Ótima descoberta! Encontrei todos os índices com “ccnew” no nome usando esta consulta.

psql
\connect discourse
select tablename,indexname,indexdef from pg_indexes where indexname LIKE '%ccnew%';

incoming_referer.csv (7.2 KB)

Acontece que eu tinha nada menos que 30 deles, todos na tabela incoming_referers. Então, verifiquei se todos os índices ccnew eram realmente duplicatas usando a coluna indexdef nesta consulta.

select indexname,indexdef from pg_indexes where tablename = 'incoming_referers';

ccnew.csv (6.6 KB)

Em seguida, excluí todos com sucesso.

DROP INDEX incoming_referers_pkey_ccnew;
DROP INDEX incoming_referers_pkey_ccnew_ccnew;
DROP INDEX incoming_referers_pkey_ccnew1;
...e assim por diante para todos os 30

Naquela altura, reindexei todo o esquema novamente, mas ele não conseguiu reconstruir dois dos mesmos índices ccnew de incoming_referers e também encontrou três índices pg_toast. Excluí-os e reindexei todo o esquema mais uma vez; novamente, mais erros. Encontrei vários outros índices ccnew no esquema discourse, reindexei pela terceira vez…

Não consigo concluir uma reindexação completa sem erros; ele continua criando e, em seguida, falhando ao reconstruir novos índices ccnew a cada vez. Após 4 reconstruções completas, excluí os índices ccnew e desisti. Acho que poderia tentar reconstruir de forma não concorrente, mas isso causaria várias interrupções no serviço.

De qualquer forma, minha suposição é que a maioria dos usuários que atualizaram do PG10 para o PG12 e tentaram reindexar depois disso possui esses índices ccnew extras, e todos eles devem ser excluídos. Eles apenas ocupam espaço e multiplicam a escrita em disco (I/O) sem qualquer benefício.

Executei o equivalente dela no Data-Explorer. Foi um pouco mais fácil de gerenciar.

O que fiz foi pegar todos os índices ccnew/ccnew1/ccnew2…ccnewn, destruí-los e reindexar. Isso resolveu para mim.

ccnew é algo do PostgreSQL para marcar os índices e acho que é alguma ineficiência no processo que faz com que esses fiquem se a indexação falhar por algum motivo.

Só posso sugerir que você primeiro trabalhe na causa raiz do problema. Eu tinha tags duplicadas que precisei excluir antes de prosseguir para destruir os índices. Se eu ficasse com até uma única tag duplicada, ela não seria indexada corretamente e criaria outro índice ccnew'n' e falharia.

Você tem razão, ainda tenho duplicatas — não é possível reconstruir os índices não-ccnew de forma não concorrente. Preciso remover essas linhas duplicadas.

discourse=# reindex index index_incoming_referers_on_path_and_incoming_domain_id;
ERROR:  could not create unique index "index_incoming_referers_on_path_and_incoming_domain_id"
DETAIL:  Key (path, incoming_domain_id)=(/search/, 3433) is duplicated.
discourse=# reindex index index_incoming_referers_on_path_and_incoming_domain_id;
ERROR:  could not create unique index "index_incoming_referers_on_path_and_incoming_domain_id"
DETAIL:  Key (path, incoming_domain_id)=(/search/, 1861) is duplicated.

O mais estranho é que só vejo uma linha com cada um desses valores de incoming_domain_id na tabela incoming_referers. Então por que elas são duplicatas?

discourse=# select * from incoming_referers where path='/search/' AND incoming_domain_id IN (1861,3433);
  id   |   path   | incoming_domain_id 
-------+----------+--------------------
 42845 | /search/ |               1861
 40763 | /search/ |               3433
(2 rows)

@sam ou @riking, devo excluir essas duas linhas da seguinte forma:

DELETE FROM incoming_referers WHERE path='/search/' AND incoming_domain_id IN (1861,3433);

… Acho que estou aprendendo Postgres afinal, hein.

Não, isso excluirá ambos - você quer usar o WHERE para encontrar os dois ids e escolher apenas um deles para excluir.

Sua consulta está usando o índice corrompido, é por isso que você está vendo apenas uma linha para cada. Tente isto:

... where path LIKE '%/search/' ...

OK, isso trouxe 43 linhas.

discourse=# select * from incoming_referers where path LIKE '%/search/' ORDER BY incoming_domain_id;
  id   |    path     | incoming_domain_id 
-------+-------------+--------------------
   878 | /search/    |                 63
 33457 | /do/search/ |                567
  1580 | /search/    |                602
  1888 | /search/    |                663
 42983 | /search/    |               1259
  4896 | /search/    |               1788
 42845 | /search/    |               1861
  5162 | /search/    |               1861
  5176 | /search/    |               1866
 43350 | /search/    |               1905
 17238 | /search/    |               1905
 20689 | /search/    |               1982
  5781 | /hg/search/ |               1987
  8031 | /search/    |               2665
 10325 | /search/    |               3192
 11289 | /search/    |               3414
 40763 | /search/    |               3433
 42849 | /search/    |               3433
 13087 | /search/    |               3895
 13159 | /search/    |               3949
 13802 | /do/search/ |               4051
 14407 | /search/    |               4209
 14507 | /search/    |               4211
 15394 | /search/    |               4230
 15533 | /search/    |               4258
 45274 | /search/    |               5303
 20923 | /search/    |               5400
 21317 | /search/    |               5534
 22928 | /search/    |               5918
 22956 | /search/    |               5926
 37448 | /search/    |               6393
 25094 | /search/    |               6412
 25594 | /search/    |               6547
 39655 | /search/    |               6596
 27371 | /search/    |               6986
 27452 | /a/search/  |               7003
 27623 | /search/    |               7041
 31041 | /search/    |               7767
 36943 | /search/    |               8622
 37381 | /search/    |               8711
 37411 | /search/    |               8716
 40424 | /search/    |               9124
 44451 | /search/    |               9525
(43 linhas)

Então, devo executar o seguinte?

DELETE FROM incoming_referers WHERE path LIKE '%/search/' AND id IN (42845,43350,42849);

Sim, esse DELETE parece correto e deve permitir que você faça um backup limpo.

Acredite ou não, mais falhas. No primeiro conjunto, encontrei outra chave duplicada em /m/search que, por algum motivo, não foi detectada pelo wildcard %/search/ e a excluí. Reindexei novamente (cada vez levando quase uma hora!) e mais falhas apareceram, indicando uma chave duplicada em users(username_lower).

discourse=# reindex index index_users_on_username_lower;              
ERROR:  could not create unique index "index_users_on_username_lower"
DETAIL:  Key (username_lower)=(john_smith) is duplicated.

Mas o fato é que havia apenas uma linha com username_lower=john_smith! Hora de colocar o chapéu de detetive.

Ao examinar a interface administrativa do fórum, vimos que havia dois usuários distintos chamados “john_smith” e “John_Smith” — note a diferença na capitalização. Então, excluí o de tudo minúsculo no nível do fórum, já que ele não estava ativo há 4 anos, e aquele índice foi reconstruído com sucesso.

Executei mais uma reconstrução completa, cada uma levando quase uma hora, e FINALMENTE acho que estamos bem — apenas um erro, mas sem duplicatas, apenas um pg_toast ccnew. Eu o removi.

discourse=# REINDEX SCHEMA CONCURRENTLY public;               
WARNING:  cannot reindex invalid index "pg_toast.pg_toast_2783329_index_ccnew" concurrently, skipping
REINDEX

Foi uma jornada.

Essas correções de banco de dados podem ou serão automatizadas em uma futura atualização do Discourse?

É mais provável que a corrupção do índice do PostgreSQL seja corrigida em uma futura atualização do PostgreSQL, caso Peter obtenha uma reprodução consistente do problema.

Se um REINDEX concorrente falhar porque a tabela contém dados inválidos, você precisa:

  1. Corrigir os dados inválidos, como foi feito aqui.

  2. Listar os índices inválidos usando SELECT pg_class.relname FROM pg_class, pg_index WHERE pg_index.indisvalid = false AND pg_index.indexrelid = pg_class.oid;

  3. Excluir cada índice inválido listado acima usando DROP INDEX <indexname>;

  4. Tente executar o REINDEX novamente.

Após executar a atualização para a versão 2.5.0beta5 e seguir as orientações pós-atualização para reindexar o banco de dados, recebo a seguinte mensagem:

discourse=# REINDEX SCHEMA CONCURRENTLY public;
ERROR:  could not create unique index "index_plugin_store_rows_on_plugin_name_and_key_ccnew"
DETAIL:  Key (plugin_name, key)=(discourse-data-explorer, q:-10) is duplicated.

Prefiro não experimentar com isso… então, como posso excluir com segurança a duplicata?

Não. A atualização não corrompe seu índice; ela apenas aponta que seu índice já está corrompido. Você pode verificar tentando restaurar seu backup em outro servidor (mesmo que esteja executando o pg10). Ou tente recriar o índice na sua instalação atual. Não está claro o que está causando a corrupção dos índices, mas há esperança de que o pg12 tenha menor probabilidade de que isso ocorra.

Existem alguns benefícios de desempenho na atualização, mas adiar a execução dela não é uma má ideia.

Agora é tarde demais, então ainda estou buscando orientação sobre os próximos passos.

Algo como

select id, plugin_name, key from plugin_store_rows where plugin_name like '%discourse-data-explorer%'

deve retornar as linhas. Acho que é seguro o suficiente para excluí-las.

Acho que ilike seria necessário