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.

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.