Um índice que quase não está fazendo nada

Prévia grátis 4 min de leitura

O curso de nível Júnior "Migrations ActiveRecord: índices" ensinou a sintaxe do where:, com exemplos de que esta aplicação não precisava. Este curso faz a pergunta mais difícil, de nível Pleno. Não "como escrever um índice parcial", e sim "como saber se o índice que você já tem está na forma errada para o jeito como é consultado".

Um índice real, e as queries em volta dele

O db/schema.rb declara isto para a tabela submissions:

create_table "submissions", force: :cascade do |t|
  # ...
  t.integer "status", default: 0, null: false
  t.boolean "success", default: false, null: false
  t.index ["challenge_id"], name: "index_submissions_on_challenge_id"
  t.index ["created_at"], name: "index_submissions_on_created_at"
  t.index ["status"], name: "index_submissions_on_status"
  t.index ["success"], name: "index_submissions_on_success"
  t.index ["user_id", "challenge_id", "status"], name: "index_submissions_on_user_challenge_status"
  t.index ["user_id", "challenge_id"], name: "index_submissions_on_user_id_and_challenge_id"
  t.index ["user_id"], name: "index_submissions_on_user_id"
end

O index_submissions_on_success cobre todas as linhas, indexadas numa única coluna booleana. Quando este curso foi escrito, três lugares da aplicação filtravam por success, e todos também filtravam por user e challenge. Em 19 de setembro de 2026 a aplicação ganhou uma coluna status, um enum com pending, passed, failed e errored, e o índice de três colunas index_submissions_on_user_challenge_status. As queries por aluno passaram para status:

# app/policies/execution/solution_access_policy.rb
::Execution::Submission
  .where(user_id: user_id, challenge_id: challenge_id, status: :failed)
  .count

# app/controllers/catalog/lessons_controller.rb
@failed_attempts = ::Execution::Submission.where(user: current_user, challenge: @challenge, status: :failed).count

# app/queries/admin/stats_query.rb
::Execution::Submission
  .where(created_at: since..)
  .group(:success)
  .count

O success continua existindo, acompanhando o status, e o painel de administração ainda agrupa por ele. Mas essa query filtra um intervalo de created_at e conta os dois valores, então um índice só em success também não a ajuda. Nenhuma query da aplicação pergunta "quais submissões tiveram sucesso na tabela inteira", a única pergunta que um índice simples em success foi feito para responder rápido.

Vendo o planejador ignorá-lo

Este curso usa a versão com success da pergunta por aluno como exemplo: um booleano é o caso mais claro para um índice parcial. As aulas 5 e 7 voltam ao que a aplicação escolheu com status.

A tabela submissions de desenvolvimento está vazia, pequena demais para o Postgres fazer uma escolha interessante. Por isso os experimentos rodam numa tabela temporária com a forma de submissions antes do status: 60.000 linhas, 200 usuários, 30 desafios e cerca de um sucesso a cada cinco. Ela tem os mesmos dois índices, em success e em (user_id, challenge_id). Esta é a pergunta das tentativas falhas como era escrita então:

EXPLAIN SELECT * FROM scratch_submissions
  WHERE user_id = 27 AND challenge_id = 1 AND success = false;
Bitmap Heap Scan on scratch_submissions  (cost=4.40..44.88 rows=9 width=29)
  Recheck Cond: ((user_id = 27) AND (challenge_id = 1))
  Filter: (NOT success)
  ->  Bitmap Index Scan on scratch_idx_user_challenge  (cost=0.00..4.40 rows=11 width=0)
        Index Cond: ((user_id = 27) AND (challenge_id = 1))

O índice só do booleano não aparece nesse plano. O planejador escolheu o índice (user_id, challenge_id), que reduz 60.000 linhas a umas dez antes de o success entrar na conta. Depois conferiu o success como um Filter simples sobre o que voltou. Um índice numa coluna com dois valores possíveis consegue, no máximo, reduzir a busca a um lado da divisão. Contra um índice que a reduz a um punhado de linhas, ele perde sempre que a query o combina com algo mais seletivo.

Pontos-chave

  • O index_submissions_on_success é um índice simples numa coluna booleana, e continua no schema.
  • As queries por aluno agora filtram status junto com user e challenge, atendidas pelo índice de três colunas. A única query que ainda lê success agrupa um intervalo de datas por ele, então nenhuma query usa o índice booleano do jeito para o qual foi feito.
  • Numa tabela de rascunho de 60.000 linhas, a query de tentativas falhas nunca tocou o índice só do booleano. Usou o índice (user_id, challenge_id) e filtrou o success depois.
  • Um índice numa coluna de dois valores só consegue dividir a tabela em duas, então perde para um índice mais seletivo sempre que uma query combina os dois.
Aula 1 de 7 · grátis

Continue com a aula 2

WHERE success = false: combinando com a query real

Um índice parcial com a forma da query de tentativas falhas tira do plano o filtro sobre success.

Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.

Digite para buscar...