Al trabajar con aplicaciones empresariales conectadas a SQL Server, una de las tareas más comunes pero esquivas es auditar con precisión qué está ocurriendo detrás de escena. Mientras que herramientas clásicas como SQL Profiler muestran con exactitud las consultas enviadas, carecen de una forma nativa de reportar la cantidad de registros devueltos por un simple SELECT, ya que funciones como @@ROWCOUNT solo aplican para operaciones DML como INSERT, UPDATE o DELETE.
El problema: Rastreo sin métricas de volumen
Durante el análisis de una aplicación desarrollada en VB.NET y ASP.NET conectada a SQL Server, me encontré rastreando consultas generadas automáticamente mediante un ORM (en este caso, LINQ to SQL). La traza en SQL Profiler mostraba sentencias idénticas a esta:
exec sp_executesql N'SELECT [t0].[idnumber], [t0].[student_idnumber], [t0].[student_code], [t0].[student_name], [t0].[status_id], [t0].[status], [t0].[motive_id], [t0].[motive], [t0].[term_id], [t0].[term], [t0].[date], [t0].[support], [t0].[observation], [t0].[usercreated], [t0].[timecreated], [t0].[usermodified], [t0].[timemodified] FROM [dbo].[vw_academic_statuses] AS [t0] WHERE ([t0].[student_code] LIKE @p0) OR ([t0].[student_name] LIKE @p1)',N'@p0 nvarchar(4000),@p1 varchar(8000)',@p0=N'%%',@p1='%%'
El inconveniente principal era que el Profiler estándar no revelaba cuántas filas devolvía la consulta al cargar la interfaz web inicial (donde los filtros llegaban vacíos como %%). Esto provocaba un consumo oculto y desmedido de recursos.
La solución: Capturando filas con Extended Events
Para solucionar la limitación del Profiler en consultas de solo lectura, recurrí a los Extended Events en SQL Server Management Studio (SSMS). Esta herramienta ofrece un rendimiento superior y una capacidad de auditoría mucho más detallada sin saturar el motor de base de datos.
Configuración de la sesión de Extended Events
Para capturar exactamente el volumen de datos devuelto por los comandos SELECT, configuré una nueva sesión seleccionando eventos clave como rpc_completed y sql_statement_completed. Dentro de las acciones globales disponibles en el asistente, incorporé campos esenciales para el diagnóstico:
sql_text: Para visualizar la consulta completa ejecutada.num_response_rows/row_count: El parámetro crítico para identificar cuántas filas retornó la sentencia.logical_reads: Para medir el impacto en memoria de la consulta.client_hostnameydatabase_name: Para contextualizar el origen y el destino.
logical_reads 193
object_name sp_executesql
result OK
row_count 1384
sql_text (@p0 nvarchar(4000),@p1 varchar(8000))SELECT [t0].[idnumber], ... FROM [dbo].[vw_academic_statuses] AS [t0] WHERE ...
row_count arrojó un valor de 1384 registros devueltos en la carga inicial del formulario, simplemente porque los filtros dinámicos no se habían aplicado todavía y el ORM ejecutó la consulta completa sin restricciones. Un consumo totalmente innecesario para un simple parpadeo de interfaz web.
Estrategias de optimización: LINQ to SQL y MSSQL
Identificar el volumen de datos devuelto fue solo el primer paso; el objetivo real era frenar el consumo innecesario de recursos. Analizando el comportamiento de la aplicación, el formulario maneja filtros dinámicos (búsquedas por código de estudiante o nombres completos mediante métodos Contains en LINQ), los cuales se traducen en cláusulas LIKE '%texto%'.
1. Dónde aplicar el límite de registros (TOP / Take)
Una duda habitual es si se debe restringir el volumen de datos desde la aplicación (VB.NET) o directamente en el motor de base de datos (MSSQL). La respuesta definitiva es hacerlo siempre en MSSQL mediante TOP o OFFSET-FETCH (o Take() en LINQ).
2. Análisis del uso de Contains en LINQ
Dado que los usuarios realizan búsquedas combinadas —por ejemplo, buscando el carnet exacto 2025-15-0023 o partes del nombre como Guillermo Antonelo Palacios Duarte—, el uso de Contains en LINQ está justificado para búsquedas parciales. Sin embargo, hay que tener en cuenta que un LIKE '%val%' impide que SQL Server aproveche eficientemente los índices tradicionales basados en B-Tree, desencadenando escaneos completos si la tabla crece demasiado.
Para mitigar esto en escenarios donde la consulta inicial carece de filtros, la implementación óptima en la capa de datos consiste en asegurar un límite por defecto (como Take(100)) mientras los campos de búsqueda se mantengan vacíos:
var query = context.vw_academic_statuses.AsQueryable();
if (!string.IsNullOrEmpty(studentCode))
query = query.Where(x => x.student_code.Contains(studentCode));
if (!string.IsNullOrEmpty(studentName))
query = query.Where(x => x.student_name.Contains(studentName));
// Limitar resultados por defecto para evitar cargas masivas
var result = query.Take(100).ToList();
La historia detrás de la nota
Descubrir que una simple vista devolvía más de mil registros silenciosos en cada carga inicial me demostró la importancia de combinar Extended Events con un control estricto de paginación en el ORM.