Busca e filtragem quebradas em apps CRUD com IA: como corrigir
Busca e filtragem quebradas podem tornar apps CRUD com IA inutilizáveis. Aprenda a consertar query builders, restringir filtros, prevenir injeção e acelerar consultas com índices.

Como é uma “busca quebrada” em um app CRUD
Busca quebrada dá a sensação de que o app está mentindo. Você digita um nome que sabe existir e ele não aparece. Ou filtra por status e de repente obtém registros que claramente não batem.
Os sintomas mais comuns são diretos:
- Resultados faltando
- Linhas duplicadas
- Ordenação que parece aleatória
Duplicatas frequentemente vêm de joins. Um registro vira várias linhas porque a consulta juntou outra tabela e nunca agrupou ou deduplicou. Ordenação “aleatória” geralmente ocorre quando não há uma ordenação estável, então o banco devolve linhas na ordem que for conveniente.
Esses problemas pioram conforme os dados crescem. Com 50 registros pode ser que você não note que um filtro às vezes é ignorado. Com 50.000 registros o mesmo bug vira timeouts, páginas parcialmente carregadas e usuários desistindo porque não encontram nada.
Quando a busca fica pouco confiável, as pessoas param de confiar no app inteiro. Elas assumem que os dados estão errados, não a consulta. Chamados de suporte aumentam e equipes passam a manter planilhas próprias porque o sistema de registro não parece seguro.
Uma forma rápida de reproduzir o problema é criar alguns registros fáceis de diferenciar. Por exemplo: um cliente chamado "Ann Lee" (ativo), um chamado "Anne Li" (inativo) e um chamado "Bob" (ativo). Aí cheque:
- Buscar "Ann" e confirmar quais registros aparecem
- Filtrar
status = activee confirmar que "Anne Li" some - Ordenar por data de criação duas vezes e confirmar que a ordem se mantém
Se algum resultado te surpreende, a busca está quebrada, mesmo que falhe “às vezes”.
Por que apps CRUD com IA normalmente erram em buscas e filtros
A maioria dos apps CRUD com IA nasce como demos rápidas. Busca e filtros são adicionados no fim e acabam sendo a parte que o usuário toca em cada página. Por isso procurar bugs nessa área é tão comum: foi feito com pressa, com padrões colados que parecem ok até os dados reais e usuários reais aparecerem.
Uma causa frequente é um query builder gerado pela IA que mistura parameterização segura com concatenação de strings. Pode bindar valores de forma segura em um ponto e depois injetar diretamente uma coluna de ordenação, operador ou um fragmento raw de WHERE em outro. Isso cria bugs confusos (resultados errados, linhas faltando) e risco real (injeção SQL por filtros “inteligentes”).
Outro problema é deixar a UI enviar qualquer coisa: qualquer nome de campo, qualquer operador, qualquer valor. Dá a sensação de flexibilidade, mas o backend acaba adivinhando a intenção. Um usuário busca status, outro usa createdAt, e alguém tenta contains em uma coluna numérica. Mesmo quando nada quebra, o comportamento fica inconsistente e difícil de testar.
Joins pioram a situação. Buscar através de tabelas juntadas sem um plano leva a duplicatas, correspondências perdidas e consultas lentas. Uma página “Customers” pode juntar orders e notes e aplicar o termo de busca em ambos. Sem regras claras para agrupar e deduplicar, um cliente com muitos pedidos pode aparecer várias vezes e a paginação fica instável.
O desempenho também falha pelo mesmo motivo: o banco não está indexado para o que as pessoas realmente fazem. Equipes indexam id e consideram o trabalho feito, enquanto consultas de produção filtram por tenant_id + status, ordenam por created_at e buscam por email.
Segurança em primeiro lugar: pare com filtros dinâmicos inseguros
Busca e filtragem quebradas muitas vezes começam com uma caixa de filtro “flexível”: usuários podem passar qualquer campo, qualquer operador e qualquer valor. Se seu app costura esses pedaços numa string SQL, um atacante pode contrabandear SQL extra para dentro da consulta. Em termos práticos, eles não estão só filtrando linhas, estão mudando o que o banco executa.
Um exemplo comum é um filtro como status=active que vira WHERE status = 'active'. Se alguém submeter active' OR 1=1 --, a query pode virar “retorne tudo”. Em casos piores, o texto injetado pode ler tabelas sensíveis ou modificar dados, dependendo das permissões.
Escapar não é o mesmo que parametrizar. Escapar tenta tornar caracteres perigosos seguros dentro de uma string. Parametrização (prepared statements) mantém a estrutura SQL fixa e manda os valores separadamente, para que o banco os trate como dados, não como instruções.
A parte complicada é que muitos problemas de “filtro dinâmico” não são sobre valores. Essas entradas são especialmente arriscadas porque a maioria das bibliotecas SQL não consegue parametrizá-las:
- Nomes de campo (exemplo:
sortBy=price) - Direção de ordenação (
asc/desc) - Operadores (
=,LIKE,>,IN) - Trechos raw de SQL (
where=...,order=...) - Nomes de tabela ou relacionamento
Para esses, não escape e espere que dê certo. Use allowlists: defina exatamente quais campos podem ser filtrados ou ordenados, quais operadores são permitidos por campo e como cada um mapeia para SQL seguro.
Também limite o dano com roles de banco com privilégios mínimos. Mesmo que uma query ruim passe, a conta usada pelo app não deveria poder dropar tabelas ou ler dados administrativos.
Crie um contrato de filtros claro (o que é permitido e o que não é)
A maioria das filtragens quebradas acontece porque o app aceita “qualquer coisa” da UI e tenta transformar isso em uma consulta. Um contrato de filtros corrige isso definindo regras claras sobre quais filtros existem, o que significam e como tratar entradas inválidas.
Mantenha o contrato pequeno. Comece com uma whitelist curta de campos filtráveis e os operadores que você suportará para cada um. Por exemplo: status pode ser equals mas não contains. Um createdAt pode ser before e after, não texto livre.
Valide tipos antes de construir a query. Trate cada filtro como uma entrada tipada, não uma string para colar no SQL: strings com comprimento máximo, números com limites de intervalo, datas estritas, enums que devem corresponder a valores permitidos, booleanos que só aceitam true/false.
Adicione restrições para que uma requisição não sobrecarregue o banco: tamanho máximo de página, número máximo de filtros por requisição e comprimento máximo do texto de busca.
Por fim, escolha regras consistentes para valores vazios, null e desconhecidos. Se um valor de filtro está vazio, você o ignora ou rejeita? Se um nome de campo é desconhecido, você retorna um erro de validação claro? Essas decisões evitam bugs do tipo “por que meus resultados desapareceram?”.
Passo a passo: refatore um query builder que retorna resultados errados
Busca e filtragem quebradas muitas vezes começam com um query builder que tenta ser “flexível” juntando strings SQL. Funciona na demo, depois retorna resultados estranhos, quebra com aspas ou vira risco de segurança.
Um caminho prático para refatorar
Primeiro, escreva o que a UI realmente precisa, em linguagem simples. Por exemplo: “Buscar clientes por nome ou email”, “Filtrar por status”, “Ordenar pelos mais recentes”, “Mostrar 25 por página”. Se você não consegue descrever claramente, o código não vai se manter limpo.
Então refatore em passos pequenos:
- Trave as entradas permitidas: quais filtros existem, quais opções de ordenação existem e para quais colunas elas mapeiam.
- Construa o WHERE apenas com parâmetros (sem concatenação de strings para valores).
- Trate chaves de filtro como não confiáveis: mapeie chaves da UI como
statuspara colunas conhecidas comocustomers.status, e rejeite ou ignore chaves desconhecidas. - Torne a ordenação estável: adicione um critério de desempate (exemplo:
created_atentãoid) para que a paginação não pule ou repita linhas. - Deixe as regras de paginação explícitas: comece com
limiteoffset, ou use paginação por cursor depois, mas não misture estilos.
Se a UI enviar sort=createdAt, não passe isso direto para o SQL. Traduza para um trecho fixo e seguro como ORDER BY customers.created_at DESC, customers.id DESC.
Testes que pegam os bugs “parecia ok”
Alguns testes focados evitam regressões:
- Nomes com apóstrofos (O'Connor)
- Emojis e nomes não latinos
- Caso misto (alex vs Alex)
- Busca vazia com filtros aplicados
- Chaves de filtro desconhecidas (ignoradas ou rejeitadas, consistentemente)
Escolha o comportamento de busca certo (e mantenha previsível)
Muita busca e filtragem aparentemente quebrada é, na verdade, regras pouco claras. Se os usuários não sabem o que a caixa de busca significa, todo resultado parece errado, mesmo quando o SQL faz exatamente o que você definiu.
Escolha um modo de busca padrão e mantenha-o em todo o app. Misturar igualdade exata numa tela e “contains” em outra é uma forma comum de confundir usuários.
Equality (igualdade exata) é melhor para IDs e emails. Prefix match funciona bem para nomes e códigos e pode ser rápido com o índice certo. Contains ajuda, mas é fácil torná-lo lento em tabelas grandes. Fuzzy matching é útil quando erros de digitação são esperados, mas deixe isso claro para o usuário.
Se usar contains, cuidado com padrões como LIKE '%term%' em tabelas grandes. O wildcard no começo costuma forçar varredura completa da tabela.
Qualquer que seja sua escolha, normalize a entrada sempre da mesma forma: remova espaços nas bordas, normalize caixa quando isso não altera o significado e decida como tratar pontuação. Buscar " Acme, Inc " deve se comportar como "acme inc", mas buscar "C++" não deveria virar silenciosamente "c".
Faça rápido: adicione os índices certos para suas consultas reais
Se os usuários dizem que a busca está lenta, comece encontrando as poucas consultas que mais pesam. Não chute. Puxe os maiores culpados dos logs do banco, traces da API ou mesmo um log simples com timestamps ao redor do endpoint de busca.
Índices funcionam melhor quando combinam com padrões reais: as colunas no WHERE, mais a forma como você ordena. Se a UI filtra por status e data e ordena pelos mais recentes, indexar só status não vai ajudar muito.
Evite indexar tudo “só por precaução”. Cada índice adiciona custo: gravações mais lentas, migrações mais pesadas e mais coisa para manter. Adicione alguns índices direcionados e reavalie o desempenho com tamanho de dados realista.
Paginação e ordenação que não quebram sob carga
Bugs de paginação aparecem como “a página 2 repete itens da página 1” ou “algumas linhas desaparecem”. A causa raiz é quase sempre ordenação instável. Se ordenar só por created_at, muitas linhas têm o mesmo timestamp, então o banco pode devolver empates em qualquer ordem. Quando novas linhas são inseridas entre requisições, a ordem muda e itens são pulados ou repetidos.
Use uma ordenação estável com desempate, por exemplo ORDER BY created_at DESC, id DESC. O id torna a posição de cada linha única, então “próxima página” permanece previsível.
Paginação por offset (LIMIT 50 OFFSET 5000) é simples, mas fica mais lenta conforme o offset cresce. Para tabelas grandes, paginação por cursor (keyset) costuma ser melhor: em vez de “página 101”, você pede “as próximas 50 linhas depois deste último visto (created_at, id)”.
Contagens totais podem silenciosamente se tornar sua consulta mais lenta. Um COUNT(*) filtrado em uma tabela grande pode executar muito trabalho, e fazê-lo em toda requisição prejudica. Alternativas comuns são mostrar contagens só quando necessário, cachear contagens comuns ou retornar hasNextPage usando LIMIT pageSize + 1 em vez de contar tudo.
Como depurar buscas e filtros lentos rapidamente
Quando busca e filtragem aparecem como “funciona, mas está lento”, trate como um problema de medição primeiro. Chutar leva a mudanças aleatórias de índice e novos bugs.
Comece capturando o SQL real das requisições lentas. Mantenha escopo por ID de requisição ou janela de tempo curta e evite registrar entradas do usuário que possam conter emails, tokens ou outros dados sensíveis. Registrar a forma dos filtros (quais campos foram usados) frequentemente é suficiente.
Depois execute EXPLAIN (ou equivalente do seu banco) na query exata que foi executada. Procure saber se o banco está usando índice ou escaneando a tabela inteira e ordenando grandes conjuntos de resultados.
Matadores de desempenho comuns:
- N+1 queries (uma query para as linhas, depois uma por linha para dados relacionados)
- Joins sem restrição (juntando tabelas grandes sem filtro seletivo)
- Falta de LIMIT (ou paginação que ainda ordena a tabela inteira)
- Filtros que bloqueiam uso de índice (funções em colunas, wildcards no início como
%term) - Ordenação por coluna sem índice
Se você não consegue reproduzir a lentidão na sua máquina, construa um dataset mínimo que ainda a mostre. Se a lentidão desaparecer, o problema costuma ser distribuição dos dados, não só o código.
Erros comuns a evitar ao consertar busca
Escapar entrada do usuário é bom, mas não torna a consulta inteira segura. Um bug comum em apps CRUD com IA é escapar valores enquanto ainda permite que o cliente envie nomes de coluna brutos como ?sort=users.email ou ?filter[field]=status. Se alguém controla nomes de coluna, operadores ou fragmentos SQL, ainda pode quebrar sua query.
Deixar o cliente escolher qualquer coluna de ordenação é outra armadilha. Causa erros (ordenar por uma coluna que você não selecionou), vazamento de dados (ordenar por um campo interno) e problemas de desempenho (ordenar por coluna sem índice). Mantenha ordenação em uma pequena allowlist que você realmente suporte.
Filtrar por campos computados também dá dor de cabeça. Filtrar por full_name quando ele é construído de first_name + last_name, ou “dias desde último login” calculado em código, tende a ficar lento ou inconsistente. Se precisar filtrar por isso, considere armazenar, indexar ou cachear o valor.
Cuidado para não consertar velocidade mudando resultados (ou consertar resultados tornando lento). Trocar LEFT JOIN por INNER JOIN pode acelerar, mas remover registros silenciosamente. Adicionar DISTINCT para esconder duplicatas pode mascarar um bug de join e confundir paginação e contagens.
Checklist rápido antes de enviar a correção
Teste os casos chatos. A maioria dos bugs se esconde em entradas fora do comum, combinações inesperadas e desempenho com dados reais.
Checagens de correção: aspas, sinais de porcentagem, underscores, emojis, texto muito longo, múltiplos espaços e busca vazia combinada com filtros. Um bom resultado é previsível, mesmo quando a entrada é bagunçada.
Checagens de segurança: o backend aceita só um pequeno conjunto de campos e operadores, e todo valor é passado como parâmetro (placeholders), nunca colado em SQL.
Checagens de desempenho: meça consultas lentas antes e depois das mudanças usando o mesmo dataset e as mesmas entradas. Documente os índices nos quais você confia para que mudanças futuras não apaguem o trabalho.
Checagens de estabilidade: ordenação é determinística (com desempate como id) e paginação não pula nem repete itens quando novas linhas chegam.
Exemplo: corrigindo a busca de “Customers” em um app CRUD com IA
Um caso comum aparece na tela “Customers”: filtros por status (active, paused), plano (Free, Pro), um intervalo de datas de signup e uma busca rápida por nome.
Os sintomas parecem aleatórios. “Active + Pro” retorna clientes que não são Pro, busca por nome perde correspondências óbvias e a ordem da lista muda a cada atualização. Sob carga, a página fica lenta a ponto de dar timeout.
O que costuma ter dado errado:
- Um join em plans ou subscriptions multiplica linhas, então um cliente aparece várias vezes e os contadores ficam errados.
- Ordenação é construída a partir de input bruto (inseguro e instável).
- O banco escaneia demais porque não há índice que combine filtragem + ordenação reais.
Uma correção limpa começa tornando filtros entediantes e estritos: permita só campos conhecidos, valide tipos e construa queries a partir de um pequeno contrato (status é um de X, plan é um de Y, datas são datas reais, name é string simples).
Depois torne resultados previsíveis: aplique filtros primeiro, traduza chaves de ordenação através de uma allowlist e adicione desempate estável (por exemplo, created_at então id) para que a paginação não embaralhe itens.
Finalmente, adicione índices direcionados que correspondam a como as pessoas realmente buscam. Para essa tela, geralmente significa um índice composto que cubra a combinação de filtro + ordenação mais comum, além de uma abordagem separada para busca por nome.
Se você herdou uma base gerada por IA (Lovable, Bolt, v0, Cursor, Replit) com query builders emaranhados e filtros dinâmicos inseguros, FixMyMess (fixmymess.ai) pode começar com uma auditoria de código gratuita para identificar joins errados, SQL arriscado e as queries específicas a refatorar primeiro. Muitos projetos podem ser reparados e preparados para deploy em 48–72 horas depois que o contrato de filtros estiver definido.
Perguntas Frequentes
How can I tell if my app’s search is actually broken or just “quirky"?
Comece verificando três coisas: correspondências ausentes que você espera ver, linhas duplicadas e ordenação inconsistente. Crie um pequeno conjunto de registros óbvios (nomes parecidos e status diferentes) e repita as mesmas ações de busca, filtro e ordenação. Se os resultados mudam ou te surpreendem, a busca está quebrada mesmo que falhe só às vezes.
Why do I see duplicate rows after adding search across related tables?
Joins frequentemente multiplicam linhas. Um registro pai (como um cliente) pode corresponder a muitas linhas relacionadas (como pedidos) e a consulta retorna uma linha por casamento do join, a menos que você agrupe ou deduplique corretamente. Isso também pode quebrar a paginação porque o banco está paginando linhas, não clientes únicos.
Why does the sort order look random even when I’m sorting by date?
Isso geralmente significa que você não tem uma ordenação estável. Se ordenar apenas por uma coluna não única (como created_at), muitas linhas empatarão e o banco pode devolver os empates em qualquer ordem. Adicione um critério de desempate, por exemplo created_at mais id, para que os resultados sejam determinísticos entre atualizações e páginas.
Why is letting the UI send any filter field or operator a bad idea?
Porque flexibilidade é perigosa. Se o backend permitir que o cliente envie nomes de campo arbitrários, operadores ou fragmentos SQL, você terá comportamento inconsistente e risco real de injeção. A abordagem segura é uma allowlist: defina exatamente quais campos podem ser filtrados/ordenados e traduza as chaves da UI para colunas SQL conhecidas.
What’s the safest way to build dynamic filters without SQL injection?
Parametrize valores sempre que possível e nunca cole a entrada do usuário dentro de strings SQL. Para as partes que não podem ser parametrizadas (como coluna de ordenação, direção e operador), use allowlists e mapeie cada opção permitida para um trecho SQL fixo. Escapar não substitui parameterização.
What is a “filter contract,” and what should it include?
É um documento pequeno e explícito. Escolha uma lista curta de filtros suportados, defina quais operadores cada filtro permite, valide tipos (enums, datas, números, booleanos) e decida comportamentos consistentes para valores vazios ou entradas desconhecidas. Um contrato estrito evita bugs de "às vezes funciona" e facilita testes.
How do I debug slow search and filtering without guessing?
Capture ou registre o SQL exato das requisições lentas (sem despejar entradas sensíveis), depois execute a ferramenta explain do banco nessa query. Procure por varreduras completas de tabela, grandes ordenações, joins sem limite e padrões que impedem uso de índice (como contains com wildcard no início). Corrija a pior consulta primeiro antes de adicionar índices aleatórios.
What indexes usually help the most for CRUD search screens?
Indexe as colunas que você realmente filtra e ordena juntas. Por exemplo, se as consultas de produção filtram por tenant_id e status e ordenam por created_at, um índice composto que combine esse padrão costuma ajudar. Não indexe tudo; adicione alguns índices direcionados e reavalie com dados realistas.
Why does pagination repeat items or skip records under load?
Paginação por offset fica mais lenta conforme o offset cresce, e ordenação instável causa repetições ou perdas entre páginas. Use ordenação estável com critério de desempate e considere paginação por cursor (keyset) para tabelas grandes. Também tenha cuidado com COUNT(*) em toda requisição; ele pode se tornar a consulta mais lenta. Em muitos casos, use LIMIT pageSize + 1 para retornar hasNextPage em vez de contar tudo.
How do I know if my AI-generated CRUD app needs professional help to fix search?
Se o código foi gerado por ferramentas como Lovable, Bolt, v0, Cursor ou Replit, fique atento a SQL concatenada em strings, chaves de ordenação controladas pelo cliente, regras de busca inconsistentes entre telas e joins que criam duplicatas. Se quiser um caminho rápido, FixMyMess pode começar com uma auditoria de código gratuita para localizar filtros inseguros e as queries específicas a refatorar; muitos projetos ficam prontos para deploy em 48–72 horas uma vez que o contrato de filtros esteja definido.