Elimina o N+1 de categoria da home e do filtro do catálogo
Fase 17, com medição antes e depois. Os N+1 não cresciam com o tamanho
do catálogo, cresciam com o número de categorias — por isso o seed de 10
produtos os escondia. Medido com 23 categorias de topo:
home antes 118 queries, 242 ms depois 9 queries, 29 ms
catálogo antes 40 queries, 60 ms depois 17 queries, 41 ms
Três correções:
* A capa de cada categoria na home era uma busca isolada, e cada uma
arrastava anexo, blob e variant records atrás de si — 5 queries por
categoria de topo. Agora são duas leituras fixas: um DISTINCT ON
(category_id) escolhe as capas, e uma segunda carrega as escolhidas
com o anexo em lote. O custo acompanha o número de categorias, não o
tamanho do catálogo.
* O filtro lateral do catálogo mostra o breadcrumb de cada categoria, e
breadcrumb_name/self_and_descendant_ids sobem e descem a árvore uma
query por nível. Daí Category::Tree, que carrega a árvore inteira em
uma leitura e responde em memória. A home passa a usar a mesma árvore:
a travessia manual que ela tinha era essa lógica duplicada.
* Catálogo e PDP perguntavam `any?` em relação não carregada antes de
renderizar, um SELECT ... LIMIT 1 jogado fora por página. Três queries
constantes a menos.
Category#breadcrumb_name continua existindo e em uso: a PDP e o admin
renderizam uma categoria só, e para isso includes(category: :parent) já
resolve. Category::Tree é para páginas que renderizam a árvore inteira, e
carrega sempre a árvore completa de propósito — um breadcrumb calculado
sobre um recorte devolveria um caminho truncado, sem erro nenhum.
Achado de método que mudou uma conclusão: contar toda notificação de
sql.active_record superestima o problema, porque o query cache do Active
Record serve repetições idênticas dentro da mesma requisição. Com a
contagem corrigida (separando payload[:cached]), uma otimização que eu já
tinha escrito — combinar average_rating e reviews_count numa leitura só,
memoizada — valia 1 query, não 6, ao custo de SQL cru e de uma memoização
que envelhece no objeto. Revertida (§3, §51, §70).
Também deliberadamente de fora: índices para price_cents/name (o plano
atual é Seq Scan + Sort, o PostgreSQL ignoraria — §53) e o mesmo N+1 de
breadcrumb no admin, que não foi medido e é de baixo tráfego.
Os testes assertam crescimento, nunca um total absoluto: o total muda a
cada alteração legítima de página, a inclinação é que não pode voltar.
Ambos os guards falham no código anterior (home 13→33 queries, catálogo
13→15). Um quarto teste que escrevi NÃO falhava no código antigo — a
redundância era absorvida pelo query cache — e foi removido em vez de
deixar confiança falsa. O comportamento de qual produto vira capa de
categoria não tinha teste nenhum antes deste refactor; agora tem.
spec/rails_helper.rb passa a apontar file_fixture_path para
test/fixtures/files, que as duas suítes compartilham, em vez de duplicar
o PNG de exemplo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPSKmT4DrESfWGVvvEJ29Z U
Uriel Juliatti committed
5ed56f20f89933f312624a7d1070745ffd39d575
Parent: a17c75c