Um índice que quase não está fazendo nada
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
statusjunto comuserechallenge, atendidas pelo índice de três colunas. A única query que ainda lêsuccessagrupa 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 osuccessdepois. - 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.
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.