Búsqueda y filtrado rotos en apps CRUD con IA: cómo arreglarlo
La búsqueda y el filtrado rotos pueden hacer que las apps CRUD con IA sean inutilizables. Aprende a arreglar builders de consultas, restringir filtros, prevenir inyección y acelerar consultas con índices.

Cómo se ve una “búsqueda rota” en una app CRUD
La búsqueda rota da la sensación de que la app miente. Escribes un nombre que sabes que existe y no aparece. O filtras por estado y de repente obtienes registros que claramente no coinciden.
Los síntomas más comunes son sencillos:
- Resultados faltantes
- Filas duplicadas
- Ordenación que parece aleatoria
Los duplicados a menudo vienen de joins. Un registro se convierte en muchas filas porque la consulta hace join con otra tabla y nunca agrupa ni deduplica. La ordenación “aleatoria” suele ocurrir cuando no hay un orden estable, así que la base de datos devuelve las filas en el orden que le resulta conveniente.
Estos problemas empeoran a medida que crecen los datos. Con 50 registros puede que no notes que a veces se ignora un filtro. Con 50.000 registros, el mismo bug se convierte en timeouts, páginas parcialmente cargadas y usuarios que se rinden porque no encuentran nada.
Cuando la búsqueda deja de ser fiable, la gente deja de confiar en toda la app. Asumen que los datos están mal, no la consulta. Suben los tickets de soporte y los equipos empiezan a llevar sus propias hojas de cálculo porque el sistema de registro ya no les da seguridad.
Una forma rápida de reproducir el problema es crear unos pocos registros fáciles de distinguir. Por ejemplo: un cliente llamado "Ann Lee" (activo), uno llamado "Anne Li" (inactivo) y uno llamado "Bob" (activo). Luego comprueba:
- Buscar "Ann" y confirmar qué registros aparecen
- Filtrar
status = activey confirmar que "Anne Li" desaparece - Ordenar por fecha de creación dos veces y confirmar que el orden se mantiene
Si algún resultado te sorprende, la búsqueda está rota, incluso si sólo falla “a veces”.
Por qué las apps CRUD con IA suelen tener búsquedas y filtros mal
La mayoría de las apps CRUD con IA empiezan como demos rápidas. La búsqueda y los filtros se añaden tarde y terminan siendo la parte que los usuarios tocan en cada página. Por eso es tan común que estén rotos: se construyen bajo presión, con patrones copiados que parecen bien hasta que llegan datos reales y usuarios reales.
Una causa frecuente es un constructor de consultas generado por IA que mezcla parametrización segura con concatenación de cadenas. Puede enlazar valores de forma segura en un sitio y luego inyectar directamente una columna de orden, un operador o un fragmento WHERE en otro. Eso crea bugs confusos (resultados erróneos, filas que faltan) y riesgos reales (inyección SQL via filtros “ingeniosos”).
Otro problema es dejar que la UI envíe cualquier cosa: cualquier nombre de campo, cualquier operador, cualquier valor. Parece flexible, pero el backend acaba adivinando la intención. Un usuario busca status, otro usa createdAt y alguien más intenta contains en una columna numérica. Incluso cuando no hay crashes, el comportamiento se vuelve inconsistente y difícil de probar.
Los joins empeoran la situación. Buscar a través de tablas con join sin un plan lleva a duplicados, coincidencias perdidas y consultas lentas. Una página de “Customers” puede hacer join con orders y notes, y luego aplicar el término de búsqueda a ambos. Sin reglas claras para agrupar y deduplicar, un cliente con muchas órdenes puede aparecer varias veces y la paginación deja de ser fiable.
El rendimiento suele fallar por la misma razón: la base de datos no está indexada para lo que la gente realmente hace. Los equipos indexan id y dan por terminado el trabajo, mientras que las consultas en producción filtran por tenant_id + status, ordenan por created_at y buscan por email.
Seguridad primero: deja de usar filtros dinámicos inseguros
La búsqueda rota suele empezar como una caja de filtros “flexible”: los usuarios pueden pasar cualquier campo, cualquier operador y cualquier valor. Si tu app pega esas piezas en una cadena SQL, un atacante puede colar SQL extra en la consulta. En términos sencillos, ya no sólo filtran filas: están cambiando lo que la base de datos ejecuta.
Un ejemplo común es un filtro como status=active que se convierte en WHERE status = 'active'. Si alguien envía active' OR 1=1 --, la consulta puede convertirse en “devolver todo”. En casos peores, el texto inyectado puede leer tablas sensibles o modificar datos, dependiendo de permisos.
Escapar no es lo mismo que parametrizar. Escapar intenta hacer los caracteres peligrosos seguros dentro de una cadena. La parametrización (prepared statements) mantiene la estructura SQL fija y envía los valores por separado, de modo que la base de datos los trata como datos, no como instrucciones.
La parte difícil es que muchos problemas con filtros dinámicos no son sobre valores. Estos inputs son especialmente riesgosos porque la mayoría de librerías SQL no pueden parametrizarlos:
- Nombres de campo (ejemplo:
sortBy=price) - Dirección de orden (
asc/desc) - Operadores (
=,LIKE,>,IN) - Fragmentos SQL crudos (
where=...,order=...) - Nombres de tablas o relaciones
Para esos casos, no escapes y ya. Usa allowlists: define exactamente qué campos se pueden filtrar u ordenar, qué operadores están permitidos por campo y cómo se mapea cada uno a SQL seguro.
También limita el daño con roles de base de datos de menor privilegio. Incluso si una consulta mala se cuela, la cuenta usada por tu app no debería poder dropear tablas ni leer datos de admin.
Crea un contrato de filtros claro (qué está permitido y qué no)
La mayoría del filtrado roto ocurre porque la app acepta “cualquier cosa” de la UI y trata de convertirla en una consulta a la base de datos. Un contrato de filtros lo arregla definiendo reglas claras sobre qué filtros existen, qué significan y cómo se maneja la entrada inválida.
Mantén el contrato pequeño. Empieza con una whitelist corta de campos filtrables y los operadores que vas a soportar para cada uno. Por ejemplo: status puede ser equals pero no contains. Una fecha createdAt puede admitir before y after, no texto libre.
Valida tipos antes de construir la consulta. Trata cada filtro como una entrada tipada, no como una cadena para pegar en SQL: cadenas con longitud máxima, números con límites de rango, fechas estrictas, enums que deben coincidir con valores permitidos, booleanos que solo pueden ser true/false.
Añade restricciones para que una petición no sobrecargue tu base de datos: un tamaño máximo de página, un número máximo de filtros por petición y una longitud máxima de texto de búsqueda.
Finalmente, elige reglas consistentes para valores vacíos, null y desconocidos. Si un valor de filtro está vacío, ¿lo ignoras o lo rechazas? Si un nombre de campo es desconocido, ¿devuelves un error de validación claro? Estas decisiones evitan bugs del tipo “¿por qué desaparecieron mis resultados?”.
Paso a paso: refactoriza un constructor de consultas que devuelve resultados incorrectos
La búsqueda rota a menudo comienza con un query builder que intenta ser “flexible” pegando cadenas SQL. Funciona para una demo, luego devuelve resultados raros, falla con comillas o se convierte en un riesgo de seguridad.
Camino práctico para refactorizar
Primero, escribe qué necesita realmente la UI, en lenguaje claro. Por ejemplo: “Buscar clientes por nombre o email”, “Filtrar por status”, “Ordenar por más recientes”, “Mostrar 25 por página”. Si no lo puedes describir claramente, el código no se mantendrá limpio.
Refactoriza en pasos pequeños:
- Bloquea las entradas permitidas: qué filtros existen, qué opciones de orden hay y a qué columnas mapean.
- Construye el WHERE solo con parámetros (sin concatenación de cadenas para valores).
- Trata las claves de filtro como no confiables: mapea claves de UI como
statusa columnas conocidas comocustomers.status, y rechaza o ignora claves desconocidas. - Haz la ordenación estable: añade un desempate (ejemplo:
created_atluegoid) para que la paginación no salte ni repita filas. - Deja explícitas las reglas de paginación: empieza con
limityoffset, o usa paginación por cursor más tarde, pero no mezcles estilos.
Si la UI envía sort=createdAt, no lo pases tal cual a SQL. Traduce a un fragmento fijo y seguro como ORDER BY customers.created_at DESC, customers.id DESC.
Tests que atrapan bugs de “parecía bien”
Unos pocos tests enfocados previenen regresiones:
- Nombres con apóstrofes (O'Connor)
- Emojis y nombres no latinos
- Mayúsculas y minúsculas mezcladas (alex vs Alex)
- Búsqueda vacía con filtros aplicados
- Claves de filtro desconocidas (ignoradas o rechazadas, de forma consistente)
Elige el comportamiento de búsqueda correcto (y mantenlo predecible)
Mucho del filtrado roto es en realidad reglas poco claras. Si los usuarios no saben qué significa la caja de búsqueda, cada resultado parecerá incorrecto, incluso si el SQL hace exactamente lo que le pediste.
Elige un modo de búsqueda por defecto y úsalo en toda la app. Mezclar coincidencia exacta en una pantalla y “contains” en otra es una forma común de confundir a la gente.
La coincidencia exacta es mejor para IDs y emails. La coincidencia por prefijo es buena para nombres y códigos y puede ser rápida con el índice correcto. La búsqueda por contenido (contains) es útil, pero fácil de volver lenta en tablas grandes. El fuzzy es útil cuando se esperan errores tipográficos, pero debes indicarlo.
Si usas contains, ten cuidado con patrones como LIKE '%term%' en tablas grandes. Ese comodín inicial suele forzar un escaneo completo de tabla.
Sea lo que sea, normaliza la entrada siempre igual: quita espacios al inicio/fin, uniforma mayúsculas cuando no cambia el significado y decide cómo tratar la puntuación. Buscar " Acme, Inc " debería comportarse como "acme inc", pero buscar "C++" no debería convertirse silenciosamente en "c".
Hazlo rápido: añade los índices correctos para tus consultas reales
Si los usuarios dicen que la búsqueda es lenta, empieza por encontrar las pocas consultas que más duelen. No adivines. Extrae los peores offenders de logs de base de datos, trazas de API o incluso un registro con timestamps alrededor del endpoint de búsqueda.
Los índices funcionan mejor cuando coinciden con patrones reales: las columnas en tu WHERE y cómo ordenas. Si la UI filtra por status y fecha y ordena por newest, indexar solo status no ayudará mucho.
Evita indexar todo “por si acaso”. Cada índice añade coste: escrituras más lentas, migraciones más pesadas y más mantenimiento. Añade un pequeño número de índices dirigidos y vuelve a comprobar el rendimiento con un tamaño de datos realista.
Paginación y ordenación que no rompen bajo carga
Los bugs de paginación aparecen como “la página 2 repite items de la 1” o “algunas filas desaparecen”. La causa raíz suele ser una ordenación inestable. Si ordenas solo por created_at, muchas filas comparten la misma marca de tiempo, por lo que la base de datos puede devolver empates en distinto orden. Cuando se insertan nuevos registros entre peticiones, el orden cambia y se saltan o repiten items.
Usa una ordenación estable con desempate, por ejemplo ORDER BY created_at DESC, id DESC. El id hace única la posición de cada fila para que la “siguiente página” sea predecible.
La paginación por offset (LIMIT 50 OFFSET 5000) es simple, pero se vuelve más lenta a medida que crece el offset. Para tablas grandes, la paginación por cursor (keyset) suele ser mejor: en lugar de pedir “página 101”, pides “las siguientes 50 filas después de este último visto (created_at, id)”.
Los conteos totales pueden convertirse en tu consulta más lenta. Un COUNT(*) filtrado sobre una tabla grande puede hacer mucho trabajo; hacerlo en cada petición perjudica. Alternativas comunes son mostrar conteos solo cuando se necesitan, cachear conteos frecuentes o devolver hasNextPage usando LIMIT pageSize + 1 en lugar de contar todo.
Cómo depurar búsquedas y filtros lentos rápidamente
Cuando la búsqueda rota sale como “funciona pero es lenta”, trátalo primero como un problema de medición. Adivinar lleva a cambios de índices al azar y nuevos bugs.
Empieza capturando el SQL real de las peticiones lentas. Manténlo acotado a un ID de petición o una ventana de tiempo corta y evita registrar entradas crudas del usuario si pueden contener emails, tokens u otros datos sensibles. Registrar la forma de los filtros (qué campos se usaron) suele ser suficiente.
Luego ejecuta EXPLAIN (o el equivalente de tu base de datos) sobre el SQL exacto que se ejecutó. Buscas si la base de datos usa un índice o hace un scan completo y ordena grandes conjuntos de resultados.
Asesinos comunes de rendimiento:
- Consultas N+1 (una consulta por filas y luego una por cada fila para datos relacionados)
- Joins sin acotar (unir tablas grandes sin un filtro selectivo)
- Falta de LIMIT (o paginación que aun así ordena toda la tabla)
- Filtros que bloquean el uso de índices (funciones sobre columnas, comodines al inicio como
%term%) - Ordenar por una columna sin índice
Si no puedes reproducir la lentitud en tu máquina, crea un dataset mínimo que aún la muestre. Si la lentitud desaparece, el problema suele ser la distribución de datos, no solo el código.
Errores comunes a evitar al arreglar búsqueda
Escapar la entrada del usuario es bueno, pero no hace la consulta segura por completo. Un bug muy común en apps CRUD con IA es escapar valores mientras se deja al cliente enviar nombres de columnas crudos como ?sort=users.email o ?filter[field]=status. Si alguien puede controlar nombres de columnas, operadores o fragmentos SQL, todavía puede romper tu consulta.
Dejar que el cliente elija cualquier columna de orden es otra trampa. Provoca errores (ordenar por una columna que no seleccionaste), fugas de datos (ordenar por un campo interno) y problemas de rendimiento (ordenar por una columna sin índice en una tabla grande). Limita la ordenación a una allowlist pequeña y soportada.
Filtrar por campos calculados también suele traer problemas. Filtrar por full_name cuando se construye de first_name + last_name, o “días desde el último login” calculado en código, tiende a volverse lento o inconsistente. Si debes filtrar por ello, considera almacenarlo, indexarlo o cachearlo.
Ten cuidado de no arreglar la velocidad cambiando resultados (o arreglar resultados volviéndolo lento). Cambiar de LEFT JOIN a INNER JOIN puede acelerar pero eliminar registros silenciosamente. Añadir DISTINCT para ocultar duplicados puede enmascarar un bug de join y complicar la paginación y los conteos.
Lista rápida de comprobación antes de desplegar la corrección
Prueba los casos aburridos. La mayoría de bugs se esconden en entradas límite, combinaciones inesperadas y rendimiento con datos reales.
Comprobaciones de corrección: comillas, signos de porcentaje, guiones bajos, emojis, texto muy largo, múltiples espacios y búsqueda vacía combinada con filtros. Un buen resultado es predecible, incluso con entrada desordenada.
Comprobaciones de seguridad: el backend acepta solo un conjunto pequeño de campos y operadores, y cada valor se pasa como parámetro (placeholders), nunca se pega en SQL.
Comprobaciones de rendimiento: mide las consultas lentas antes y después de tus cambios usando el mismo dataset y las mismas entradas. Documenta los índices de los que dependes para que cambios futuros no borren ese trabajo accidentalmente.
Comprobaciones de estabilidad: la ordenación es determinista (con desempate como id) y la paginación no salta ni repite items cuando llegan filas nuevas.
Ejemplo: arreglar una búsqueda “Customers” rota en una app CRUD con IA
Un caso común aparece en una pantalla de “Customers”: filtrar por status (active, paused), plan (Free, Pro), un rango de fecha de signup y una búsqueda rápida por nombre.
Los síntomas parecen aleatorios. “Active + Pro” devuelve clientes que no son Pro, la búsqueda por nombre omite coincidencias obvias y el orden de la lista cambia en cada refresco. Bajo carga, la página se vuelve lo bastante lenta como para hacer timeout.
Qué suele haber salido mal:
- Un join a plans o subscriptions multiplica filas, así que un cliente aparece muchas veces y los conteos salen mal.
- La ordenación se construye desde la entrada cruda (insegura e inestable).
- La base de datos hace mucho scan porque no hay un índice que coincida con el patrón real de filtro + orden.
Un arreglo limpio empieza por hacer los filtros aburridos y estrictos: solo permitir campos conocidos, validar tipos y construir consultas desde un contrato pequeño (status es uno de X, plan es uno de Y, fechas son fechas reales, name es una cadena simple).
Luego haz los resultados predecibles: aplica filtros primero, traduce las claves de orden mediante una allowlist y añade un desempate estable (por ejemplo, created_at luego id) para que la paginación no reordene items.
Finalmente, añade índices dirigidos que coincidan con cómo la gente realmente busca. Para esta pantalla suele convenir un índice compuesto que cubra la combinación más común de filtro + orden, más un enfoque separado para la búsqueda por nombre.
Si heredaste una base de código generada por IA (Lovable, Bolt, v0, Cursor, Replit) con query builders enredados y filtros dinámicos inseguros, FixMyMess (fixmymess.ai) puede empezar con una auditoría de código gratuita para localizar los joins erróneos, SQL riesgosos y las consultas específicas que conviene refactorizar primero. Muchos proyectos pueden repararse y quedar listos para desplegar en 48–72 horas una vez que el contrato de filtros está definido.
Preguntas Frecuentes
How can I tell if my app’s search is actually broken or just “quirky”?
Comienza comprobando tres cosas: coincidencias ausentes que esperas ver, filas duplicadas y ordenación inconsistente. Crea un pequeño conjunto de registros obvios (nombres parecidos y distintos estados) y repite las mismas búsquedas, filtros y ordenaciones. Si los resultados cambian o te sorprenden, la búsqueda está rota incluso si falla sólo a veces.
Why do I see duplicate rows after adding search across related tables?
Los joins suelen multiplicar filas. Un registro padre (por ejemplo, un cliente) puede corresponder a muchas filas relacionadas (por ejemplo, órdenes), y la consulta devuelve una fila por cada coincidencia del join a menos que agrupes o deduplices correctamente. Eso también puede romper la paginación porque la base de datos está paginando filas, no clientes únicos.
Why does the sort order look random even when I’m sorting by date?
Normalmente significa que no tienes una ordenación estable. Si ordenas solo por una columna no única (como created_at), muchas filas empatan y la base de datos puede devolver los empates en cualquier orden. Añade un desempate como created_at más id para que los resultados sean deterministas entre refrescos y páginas.
Why is letting the UI send any filter field or operator a bad idea?
Porque la flexibilidad es arriesgada. Si el backend permite que el cliente envíe nombres de campo arbitrarios, operadores o fragmentos SQL crudos, obtendrás comportamiento inconsistente y un riesgo real de inyección. El enfoque seguro es una allowlist: define exactamente qué campos se pueden filtrar/ordenar y traduce las claves de la UI en columnas SQL conocidas.
What’s the safest way to build dynamic filters without SQL injection?
Parametriza valores siempre que sea posible y nunca pegues la entrada del usuario directamente en cadenas SQL. Para las partes que no se pueden parametrizar (como la columna de ordenación, la dirección o el operador), usa allowlists y mapea cada opción permitida a un fragmento SQL fijo. El escape por sí solo no sustituye a la parametrización.
What is a “filter contract,” and what should it include?
Hazlo pequeño y explícito. Elige una lista corta de filtros soportados, define qué operadores permite cada filtro, valida tipos (enums, fechas, números, booleanos) y decide un comportamiento consistente para valores vacíos o desconocidos. Un contrato estricto evita bugs de “a veces funciona” y facilita las pruebas.
How do I debug slow search and filtering without guessing?
Captura o registra el SQL exacto de las peticiones lentas (sin volcar entradas sensibles), y ejecuta la herramienta EXPLAIN de tu base de datos sobre esa consulta. Busca scans completos de tabla, grandes ordenaciones, joins sin límite y patrones que impiden el uso de índices (como búsquedas con comodín al inicio). Arregla la peor consulta primero antes de añadir índices al azar.
What indexes usually help the most for CRUD search screens?
Indexa las columnas que realmente filtras y ordenas juntas. Por ejemplo, si en producción filtras por tenant_id y status y ordenas por created_at, un índice compuesto que refleje ese patrón suele ayudar. No indexes todo; añade unos pocos índices dirigidos y vuelve a probar con datos realistas.
Why does pagination repeat items or skip records under load?
La paginación por offset se vuelve más lenta cuanto mayor es el offset, y la ordenación inestable causa repeticiones o saltos entre páginas. Usa una ordenación estable con desempate y considera la paginación por cursor (keyset) para tablas grandes. Además, ten cuidado con COUNT(*) en cada petición; puede convertirse en la consulta más lenta.
How do I know if my AI-generated CRUD app needs professional help to fix search?
Si el código fue generado por herramientas como Lovable, Bolt, v0, Cursor o Replit, fíjate en SQL concatenado en cadenas, claves de ordenación controladas por el cliente, reglas de búsqueda inconsistentes entre pantallas y joins que crean duplicados. Si quieres una vía rápida, FixMyMess puede comenzar con una auditoría de código gratuita para señalar filtros inseguros y las consultas que hay que refactorizar; muchos proyectos quedan listos para desplegar en 48–72 horas.