Construir una búsqueda paginada con filtros opcionales

Situación

Una pantalla de productos permite escribir parte del nombre, elegir una categoría y limitar un rango de precios. Todos los filtros son opcionales y el resultado debe dividirse en páginas estables.

Paginar significa devolver solo un tramo ordenado del resultado. OFFSET omite las filas de las páginas anteriores y FETCH NEXT limita cuántas filas se entregan; ambos dependen de un ORDER BY estable para que una fila no cambie de página arbitrariamente.

Preparar los parámetros

Antes de consultar, TRIM retira espacios de los extremos y NULLIF(..., N'') convierte un texto vacío en NULL. COALESCE aporta un valor predeterminado cuando un parámetro también es NULL.

DECLARE @TextoEntrada NVARCHAR(120) = N'  USB  ';
DECLARE @IdCategoria SMALLINT = NULL;
DECLARE @PrecioMinimo DECIMAL(12, 2) = NULL;
DECLARE @PrecioMaximo DECIMAL(12, 2) = 1000;
DECLARE @PaginaSolicitada INT = 1;
DECLARE @TamanoSolicitado INT = 20;

DECLARE @Texto NVARCHAR(120) =
    NULLIF(TRIM(@TextoEntrada), N'');
DECLARE @Pagina INT =
    CASE WHEN COALESCE(@PaginaSolicitada, 1) < 1
         THEN 1 ELSE COALESCE(@PaginaSolicitada, 1) END;
DECLARE @TamanoPagina INT =
    CASE WHEN COALESCE(@TamanoSolicitado, 20) BETWEEN 1 AND 100
         THEN COALESCE(@TamanoSolicitado, 20)
         ELSE 20 END;
DECLARE @Desplazamiento BIGINT =
    CONVERT(BIGINT, @Pagina - 1) * @TamanoPagina;

Una cadena vacía se convierte en NULL. La página nunca será menor que uno y el tamaño queda limitado a un rango conocido.

Consulta final

SELECT
    p.IdProducto,
    p.Nombre,
    c.Nombre AS Categoria,
    p.Precio,
    p.Existencia,
    COUNT(*) OVER () AS TotalResultados
FROM dbo.Producto AS p
INNER JOIN dbo.Categoria AS c
    ON c.IdCategoria = p.IdCategoria
WHERE p.Activo = 1
  AND
  (
      @Texto IS NULL
      OR p.Nombre LIKE N'%' + @Texto + N'%'
  )
  AND
  (
      @IdCategoria IS NULL
      OR p.IdCategoria = @IdCategoria
  )
  AND
  (
      @PrecioMinimo IS NULL
      OR p.Precio >= @PrecioMinimo
  )
  AND
  (
      @PrecioMaximo IS NULL
      OR p.Precio <= @PrecioMaximo
  )
ORDER BY
    p.Nombre,
    p.IdProducto
OFFSET @Desplazamiento ROWS
FETCH NEXT @TamanoPagina ROWS ONLY;

Recorrido

En un procedimiento, un parámetro es un valor recibido desde el exterior. En este lote utilizamos variables locales para poder ejecutar el ejemplo completo en SSMS; la aplicación real debe validar sus valores antes de llamar al procedimiento o consulta parametrizada. Cada filtro utiliza el patrón “valor ausente o condición cumplida”. Cuando el valor es NULL, esa parte no restringe el resultado.

ORDER BY incluye IdProducto como desempate. Sin un orden único, una fila puede cambiar de página entre ejecuciones aunque los datos no hayan cambiado. Un orden único no congela el catálogo: inserciones, eliminaciones o cambios de nombre entre solicitudes todavía pueden desplazar filas. Si varias páginas deben representar una sola fotografía lógica, también se necesita una estrategia de aislamiento o una marca de versión de los datos.

COUNT(*) OVER () informa cuántas filas cumplen los filtros antes de paginar. La aplicación puede calcular el número de páginas sin ejecutar otra consulta, aunque el conteo también tiene costo.

Resultado esperado

Con el texto USB y precio máximo de 1000, la consulta devuelve productos activos cuyo nombre contiene ese texto. Cada fila repite TotalResultados para describir el conjunto completo, no solamente la página.

Comprobación en SSMS

La búsqueda devuelve dos productos y repite el total del conjunto en ambas filas, antes de aplicar el límite de la página.

Resultado real en SSMS de la búsqueda paginada

Variación: ordenar por precio

Podemos cambiar a ORDER BY p.Precio, p.IdProducto. No concatenamos directamente un nombre de columna recibido del usuario. Si el orden es configurable, elegimos entre expresiones conocidas o construimos SQL dinámico —código SQL formado durante la ejecución— con una lista permitida.

Límite o precaución

Los filtros opcionales pueden producir planes de ejecución desiguales según los parámetros. Un plan es la estrategia elegida por SQL Server. En búsquedas de alto volumen medimos alternativas como procedimientos especializados, recompilación controlada o SQL dinámico parametrizado, donde los valores se envían separados del texto.

El patrón LIKE '%texto%' normalmente no puede buscar desde el inicio de un índice convencional, una estructura que organiza valores para facilitar el acceso. Para catálogos grandes quizá necesitemos búsqueda de texto completo, un índice especializado para localizar palabras y formas lingüísticas, u otra estrategia.

Los caracteres %, _ y [ tienen significado dentro de LIKE. Si la intención es buscarlos como texto, la entrada necesita un escape definido; parametrizar el valor protege la separación entre datos y código, pero no cambia esas reglas de coincidencia.

OFFSET debe recorrer las filas anteriores antes de entregar una página profunda. En catálogos grandes medimos una paginación por continuación —también llamada keyset— que retoma desde la última combinación ordenada, por ejemplo Nombre e IdProducto. También calculamos el desplazamiento en BIGINT o limitamos la página máxima para evitar desbordamientos al multiplicar valores enteros.

Relacionado con

Índice del recorrido

Casos aplicados

10 artículos de Casos aplicados
  1. 01Consultar clientes con sus pedidos confirmados
  2. 02Encontrar clientes que todavía no tienen pedidos
  3. 03Mostrar el primer teléfono disponible de cada cliente
  4. 04Obtener ventas totales por cliente y filtrar las más altas
  5. 05Insertar un pedido y sus partidas de forma segura
  6. 06Actualizar precios de una categoría con validación previa
  7. 07Combinar pedidos actuales e históricos
  8. 08Obtener el último pedido de cada cliente
  9. 09Calcular venta diaria y total acumulado
  10. 10Construir una búsqueda paginada con filtros opcionales