Ajuste del rendimiento para aplicaciones mssql-python

El controlador mssql-python ofrece varias funciones y patrones para optimizar el rendimiento de las aplicaciones SQL Server, incluyendo pooling de conexiones, optimización de consultas y operaciones masivas.

Administración de conexiones

Utilice la agrupación de conexiones

La agrupación de conexiones viene integrada. Cuando llamas conn.close(), la conexión vuelve al pool para su reutilización en lugar de ser destruida, así que las llamadas posteriores connect() se saltan el costoso apretón de manos:

import mssql_python

def get_data():
    conn = mssql_python.connect(
        "Server=<server>.database.windows.net;Database=<database>;"
        "Authentication=ActiveDirectoryDefault;Encrypt=yes"
    )
    try:
        cursor = conn.cursor()
        cursor.execute("SELECT TOP 10 Name, ListPrice FROM Production.Product")
        return cursor.fetchall()
    finally:
        conn.close()

Configurar el tamaño del pool para la carga de trabajo

Ajusta el tamaño del grupo según tus requisitos de concurrencia. Si tu aplicación gestiona muchos usuarios simultáneos, aumenta el grupo. Para cargas de trabajo más ligeras, un pool más pequeño conserva recursos del servidor:

import mssql_python

mssql_python.pooling(
    max_size=50,      # Default is 100; reduce or increase for your workload
    idle_timeout=600  # Seconds before idle connections are recycled
)

Reutilizar conexiones dentro de las operaciones

Abrir una nueva conexión para cada consulta supone una sobrecarga incluso con la agrupación de conexiones. En su lugar, mantener una sola conexión durante la duración de una operación lógica:

# Bad: New connection per query
def bad_pattern(product_ids):
    for pid in product_ids:
        conn = mssql_python.connect(connection_string)
        cursor = conn.cursor()
        cursor.execute("SELECT Name, ListPrice FROM Production.Product WHERE ProductID = %(pid)s", {"pid": pid})
        row = cursor.fetchone()
        process(row)
        conn.close()

# Good: Single connection for all queries
def good_pattern(product_ids):
    conn = mssql_python.connect(connection_string)
    cursor = conn.cursor()
    try:
        for pid in product_ids:
            cursor.execute("SELECT Name, ListPrice FROM Production.Product WHERE ProductID = %(pid)s", {"pid": pid})
            row = cursor.fetchone()
            process(row)
    finally:
        conn.close()

Mantener las conexiones abiertas en servicios de larga duración

Los servidores web, los trabajadores de cola y los trabajos programados que se ejecutan continuamente deberían mantener las conexiones abiertas en lugar de conectarse y desconectarse en cada operación. Abrir una conexión implica un handshake TCP, negociación TLS y autenticación, que puede tardar entre 50 y 200 ms dependiendo de la distancia de la red y el método de autenticación. Para un trabajador de cola que procesa miles de mensajes por hora, esa carga se acumula rápidamente.

Mantén la conexión abierta durante toda la vida del trabajador y vuelve a conectarte cuando falle. Suspensión entre iteraciones para evitar saturar el servidor cuando la cola esté vacía:

import mssql_python
import time

def run_worker(connection_string: str, poll_interval: float = 1.0):
    conn = None
    try:
        while True:
            try:
                if conn is None:
                    conn = mssql_python.connect(connection_string)
                cursor = conn.cursor()
                cursor.execute("SELECT TOP 1 * FROM dbo.JobQueue WHERE Status = 'Pending' ORDER BY CreatedDate")
                job = cursor.fetchone()
                if job:
                    try:
                        process_job(job)
                        cursor.execute("UPDATE dbo.JobQueue SET Status = 'Done' WHERE JobID = %(job_id)s", {"job_id": job[0]})
                    except Exception:
                        cursor.execute("UPDATE dbo.JobQueue SET Status = 'Failed' WHERE JobID = %(job_id)s", {"job_id": job[0]})
                    conn.commit()
                else:
                    time.sleep(poll_interval)  # No work available, wait before polling again
            except mssql_python.OperationalError:
                # Connection lost, reconnect on next iteration
                conn = None
                time.sleep(poll_interval)
    finally:
        if conn is not None:
            conn.close()

Con el pooling de conexiones activado (por defecto), el pool gestiona las conexiones inactivas por ti. Pero si desactivas la agrupación de conexiones o usas una única conexión dedicada, configura Connection Timeout y Command Timeout en tu cadena de conexión para detectar pronto las conexiones obsoletas en lugar de que la conexión se quede colgada.

Optimización de consultas

Fetch solo necesitaba datos

Seleccionar solo las columnas que utiliza tu aplicación reduce la transferencia de red, el consumo de memoria y el tiempo de ejecución de consultas.

# Bad: Select all columns
cursor.execute("SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s", {"customer_id": 1})

# Good: Select specific columns
cursor.execute("""
    SELECT SalesOrderID, OrderDate, TotalDue
    FROM Sales.SalesOrderHeader
    WHERE CustomerID = %(customer_id)s
""", {"customer_id": 1})

Utiliza métodos apropiados de obtención

El controlador proporciona varios métodos para recuperar datos. Usa la que coincida con el tamaño de tu resultado:

  • fetchval() devuelve un único valor escalar con una sobrecarga mínima.
  • fetchall() carga todo el conjunto de resultados en memoria, lo cual funciona bien para tablas pequeñas.
  • fetchmany(n) recupera filas en lotes, manteniendo el uso de memoria constante para grandes conjuntos de resultados.

El tamaño fetchmany() adecuado del lote depende del ancho de la fila. Para filas estrechas (unas pocas columnas pequeñas, aproximadamente 1 KB cada una), 1.000 filas mantienen cada lote alrededor de 1 MB de memoria. Para filas más anchas con cadenas grandes o columnas binarias, usa un lote más pequeño. Empieza con 1.000 y ajusta según tus datos.

def process_batch(rows):
    # Example: print each row. Replace with your own logic.
    for row in rows:
        print(row)

# Single value
cursor.execute("SELECT COUNT(*) FROM Production.Product")
count = cursor.fetchval()

# Small result set
cursor.execute("SELECT ProductCategoryID, Name FROM Production.ProductCategory")
categories = cursor.fetchall()

# Large result set - process in batches
cursor.execute("SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader")
while True:
    batch = cursor.fetchmany(1000)
    if not batch:
        break
    process_batch(batch)

Utilizar la paginación del lado del servidor

En lugar de recuperar todas las filas y cortar en Python, usa OFFSET/FETCH NEXT para recuperar solo la página que necesitas.

def get_page(cursor, page: int, page_size: int = 50) -> list:
    """Get paginated results efficiently."""
    offset = (page - 1) * page_size

    cursor.execute("""
        SELECT ProductID, Name, ListPrice
        FROM Production.Product
        ORDER BY ProductID
        OFFSET %(offset)s ROWS
        FETCH NEXT %(page_size)s ROWS ONLY
    """, {"offset": offset, "page_size": page_size})

    return cursor.fetchall()

Uso SET NOCOUNT de ON

Por defecto, SQL Server envía un mensaje de "filas afectadas" después de cada sentencia DML. SET NOCOUNT ON suprime estos mensajes y reduce el tráfico de red. Es una configuración a nivel de sesión, así que configúrala una vez después de conectarte en lugar de incrustarla en cada consulta.

# Set once after connecting
cursor.execute("SET NOCOUNT ON")

# All subsequent statements on this connection skip the row-count message
cursor.execute(
    "INSERT INTO Log (Message) VALUES (%(message)s)",
    {"message": "Log entry"}
)

Elige el método de inserción adecuado

El controlador ofrece tres formas de insertar datos, cada una adaptada a una escala diferente:

Método Recuento de filas Por qué
execute() 1 fila por llamada Úsalo para operaciones de una sola fila como envíos de formularios o manejadores de API donde necesitas insertar el ID inmediatamente.
executemany() ~10-1.000 filas Utiliza vinculación de parámetros columna a columna para un mejor rendimiento que un bucle. Envía cada fila como una sentencia parametrizada.
bulkcopy() Cientos de filas o más Utiliza el protocolo TDS de inserción masiva, que es significativamente más eficiente que las inserciones fila por fila. Ideal para cargas de datos, migraciones y procesamiento por lotes.

Para más detalles y ejemplos, véase Patrones de carga y movimiento de datos.

Insertos individuales con execute()

Úsalo para insertos únicos donde necesites el resultado inmediato. Production.Product tiene varias columnas NOT NULL sin valores predeterminados, por lo que el insert las lista todas:

from datetime import datetime

cursor.execute(
    """
    INSERT INTO Production.Product
        (Name, ProductNumber, SafetyStockLevel, ReorderPoint,
         StandardCost, ListPrice, DaysToManufacture, SellStartDate)
    VALUES (%(name)s, %(number)s, %(safety)s, %(reorder)s,
            %(cost)s, %(price)s, %(days)s, %(start)s)
    """,
    {
        "name": "Widget", "number": "WG-1001",
        "safety": 100, "reorder": 75,
        "cost": 12.50, "price": 19.99,
        "days": 1, "start": datetime(2024, 1, 1),
    },
)
conn.commit()

Inserciones por lotes con executemany()

executemany() enlaza parámetros columna por columna y los envía de forma eficiente. Úsalo para lotes moderados en lugar de llamar a execute() en un bucle. Tenga en cuenta que executemany() requiere marcadores posicionales ? con una lista de tuplas, mientras que execute() admite tanto ? como parámetros %(name)s con nombre con diccionarios. Consulta las consultas parametrizadas para más detalles sobre cada estilo.

rows = [
    ("Widget A", "WG-1001", 100, 75, 12.50, 19.99, 1, datetime(2024, 1, 1)),
    ("Widget B", "WG-1002", 100, 75, 15.00, 24.99, 1, datetime(2024, 1, 1)),
    ("Widget C", "WG-1003", 100, 75, 18.00, 29.99, 1, datetime(2024, 1, 1)),
]

cursor.executemany(
    """
    INSERT INTO Production.Product
        (Name, ProductNumber, SafetyStockLevel, ReorderPoint,
         StandardCost, ListPrice, DaysToManufacture, SellStartDate)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    """,
    rows,
)
conn.commit()

Copia a granel para cargas grandes

Cuando el rendimiento importa más que el control por fila, cambia a bulkcopy(). Transmite las filas a través del protocolo de inserción masiva TDS y evita la sobrecarga por fila de sentencias parametrizadas. El crossover exacto en el que bulkcopy() supera executemany() depende del ancho de fila y la latencia de la red, pero normalmente está en las bajas centenas de filas. Para lotes muy pequeños, executemany() es más sencillo porque bulkcopy() crea una conexión interna separada y confirma automáticamente la transacción.

A diferencia de execute() y executemany(), bulkcopy() asigna los valores a las columnas por posición, no mediante una lista de columnas INSERT. Pasa column_mappings para indicar las columnas de destino en las que vas a cargar datos, de modo que las tuplas de origen se alineen con las columnas adecuadas en lugar de con la primera columna de identidad de la tabla:

result = cursor.bulkcopy(
    "Production.Product",
    rows,
    column_mappings=["Name", "ProductNumber", "SafetyStockLevel", "ReorderPoint",
                     "StandardCost", "ListPrice", "DaysToManufacture", "SellStartDate"],
)
print(f"Copied {result['rows_copied']} rows")

Para cargas muy grandes, utiliza un generador para evitar cargar todo el conjunto de datos en memoria y configura batch_size para que se comprometa periódicamente:

import csv

def csv_rows(path):
    with open(path, newline="") as f:
        reader = csv.reader(f)
        next(reader)  # Skip header
        for row in reader:
            yield tuple(row)

cursor.bulkcopy(
    "Production.Product",
    csv_rows("products.csv"),
    column_mappings=["Name", "ProductNumber", "SafetyStockLevel", "ReorderPoint",
                     "StandardCost", "ListPrice", "DaysToManufacture", "SellStartDate"],
    batch_size=5000,
)

Estrategias de caché

Para datos de referencia que rara vez cambian (categorías, tablas de consulta, configuración), almacena en caché los resultados en tu aplicación en lugar de consultar en cada solicitud.

Python's functools.lru_cache proporciona una memorización sencilla, pero almacena en caché indefinidamente hasta que el proceso se reinicia. Si los datos subyacentes pueden cambiar, úsalo cachetools.TTLCache para refrescar automáticamente tras un límite de tiempo:

from cachetools import TTLCache, cached

category_cache = TTLCache(maxsize=1, ttl=300)  # Refresh every 5 minutes

@cached(category_cache)
def get_categories(connection_string: str) -> tuple:
    conn = mssql_python.connect(connection_string)
    try:
        cursor = conn.cursor()
        cursor.execute("SELECT ProductCategoryID, Name FROM Production.ProductCategory")
        return cursor.fetchall()
    finally:
        conn.close()

Optimización de la red

Minimizar los viajes de ida y vuelta

Cada consulta es un viaje de ida y vuelta de la red al servidor. Combina consultas relacionadas en un solo lote y úsalo nextset() para avanzar a través de los conjuntos de resultados:

# Bad: Multiple round trips
cursor.execute("SELECT CustomerID, AccountNumber FROM Sales.Customer WHERE CustomerID = %(customer_id)s", {"customer_id": 1})
customer = cursor.fetchone()
cursor.execute("SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s", {"customer_id": 1})
orders = cursor.fetchall()
cursor.execute("SELECT COUNT(*) FROM Sales.SalesOrderDetail WHERE SalesOrderID IN (SELECT SalesOrderID FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s)", {"customer_id": 1})
detail_count = cursor.fetchval()

# Good: Single round trip
cursor.execute("""
    SELECT CustomerID, AccountNumber FROM Sales.Customer WHERE CustomerID = %(customer_id)s;
    SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s;
    SELECT COUNT(*) FROM Sales.SalesOrderDetail WHERE SalesOrderID IN (SELECT SalesOrderID FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s);
""", {"customer_id": 1})

customer = cursor.fetchone()
cursor.nextset()
orders = cursor.fetchall()
cursor.nextset()
detail_count = cursor.fetchval()

Utilizar procesamiento en el lado del servidor para lógica compleja

Enviar agregación y filtrado a SQL Server en lugar de buscar filas en bruto y procesarlas en Python. El servidor devuelve una única fila resumen en lugar de potencialmente miles de filas de detalle:

cursor.execute("""
    SELECT p.Name, COUNT(sod.SalesOrderDetailID) AS OrderCount, SUM(sod.LineTotal) AS TotalSales
    FROM Production.Product p
    JOIN Sales.SalesOrderDetail sod ON p.ProductID = sod.ProductID
    WHERE p.ProductID = %(product_id)s
    GROUP BY p.Name
""", {"product_id": 707})

Evitar operaciones de cursor intercaladas

El controlador mssql-python no soporta Múltiples Conjuntos de Resultados Activos (MARS). Solo un cursor puede tener una consulta activa por conexión. Obtén completamente el primer conjunto de resultados antes de ejecutar la siguiente consulta, o usa una segunda conexión:

connection_string = (
    "Server=<server>.database.windows.net;Database=<database>;"
    "Authentication=ActiveDirectoryDefault;Encrypt=yes"
)

# Option 1: Fetch first, then query (single connection)
# Warning: This is an N+1 pattern. Each iteration is a round trip.
# Use this only when the JOIN in Option 2 is not possible.
conn = mssql_python.connect(connection_string)
cursor = conn.cursor()
cursor.execute("SELECT ProductID FROM Production.Product WHERE ProductSubcategoryID = 1")
product_ids = [row[0] for row in cursor.fetchall()]

for pid in product_ids:
    cursor.execute("SELECT ProductID, LocationID, Quantity FROM Production.ProductInventory WHERE ProductID = %(product_id)s", {"product_id": pid})
    inventory = cursor.fetchone()

conn.close()

# Option 2: Use a JOIN instead of N+1 queries (preferred)
conn = mssql_python.connect(connection_string)
cursor = conn.cursor()
cursor.execute("""
    SELECT p.ProductID, p.Name, i.Quantity
    FROM Production.Product p
    LEFT JOIN Production.ProductInventory i ON p.ProductID = i.ProductID
    WHERE p.ProductSubcategoryID = 1
""")
results = cursor.fetchall()
conn.close()

Administración de memoria

Procesar resultados grandes por bloques

Cargar una tabla de varios millones de filas en una lista consume memoria proporcional al conjunto completo de resultados. Usa OFFSET y FETCH NEXT para paginar los datos del lado del servidor y procesar un bloque cada vez.

def quote_id(identifier: str) -> str:
    """Quote a possibly schema-qualified SQL identifier to prevent SQL injection."""
    return ".".join("[" + part.replace("]", "]]") + "]" for part in identifier.split("."))

def process_large_table(cursor, table: str, columns: list[str], key_column: str, processor, chunk_size: int = 10000):
    """Process large table without loading all data."""
    safe_table = quote_id(table)
    safe_key = quote_id(key_column)
    col_list = ", ".join(quote_id(c) for c in columns)
    cursor.execute(f"SELECT COUNT(*) FROM {safe_table}")
    total = cursor.fetchval()

    offset = 0
    while offset < total:
        cursor.execute(f"""
            SELECT {col_list} FROM {safe_table}
            ORDER BY {safe_key}
            OFFSET ? ROWS
            FETCH NEXT ? ROWS ONLY
        """, (offset, chunk_size))

        chunk = cursor.fetchall()
        processor(chunk)

        offset += chunk_size
        print(f"Processed {min(offset, total)}/{total}")

# key_column must be unique, otherwise rows can be duplicated or skipped across pages
process_large_table(
    cursor,
    "Production.TransactionHistory",
    ["TransactionID", "ProductID", "Quantity", "ActualCost"],
    "TransactionID",
    lambda chunk: None,  # replace with your row-processing logic
)

Usa generadores para streaming

Un envolvimiento fetchmany() de generador de Python mantiene el uso de memoria constante independientemente del tamaño de la tabla. El llamador itera fila por fila sin cargar el conjunto completo de resultados. Para una fuente extragrande, combina tablas con UNION ALL y transmite el resultado combinado de la misma manera.

def stream_query(cursor, query: str, params: dict = None, batch_size: int = 1000):
    cursor.execute(query, params or {})
    
    while True:
        batch = cursor.fetchmany(batch_size)
        if not batch:
            break
        for row in batch:
            yield row

# Union the live and archive transaction tables into one extra-large result set
query = """
    SELECT TransactionID, ProductID, Quantity, ActualCost FROM Production.TransactionHistory
    UNION ALL
    SELECT TransactionID, ProductID, Quantity, ActualCost FROM Production.TransactionHistoryArchive
"""

count = 0
for row in stream_query(cursor, query, batch_size=5000):
    count += 1
print(f"Streamed {count} rows")

Limpia los recursos con rapidez

Las conexiones sin cerrar ocupan los recursos del servidor y pueden agotar el conjunto de conexiones. Utiliza un gestor de contexto para garantizar la limpieza incluso cuando ocurran excepciones.

from contextlib import contextmanager

@contextmanager
def database_connection(connection_string: str):
    conn = mssql_python.connect(connection_string)
    try:
        yield conn
    finally:
        conn.close()

with database_connection(connection_string) as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT TOP 10 Name, ListPrice FROM Production.Product")
    data = cursor.fetchall()

Supervisión del uso de la memoria

Los grandes conjuntos de resultados, las cachés de larga duración y los objetos de conexión consumen memoria. Si tu aplicación se ejecuta como un servicio, fugas de memoria de cursores no cerrados o cachés no acotadas pueden acabar provocando que el proceso sea cancelado por el sistema operativo o el entorno de ejecución del contenedor.

Usa el módulo de tracemalloc Python para capturar instantáneas de la memoria y encontrar las asignaciones más grandes.

import tracemalloc

tracemalloc.start()

# ... run your workload ...

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
for stat in top_stats[:10]:
    print(stat)

Las fuentes comunes de crecimiento inesperado de la memoria incluyen:

  • Llamar a fetchall() en una consulta que devuelve millones de filas. Usa fetchmany() o un generador en su lugar.
  • Almacenamiento en caché de resultados de consultas sin un maxsize ni TTL. Las cachés crecen hasta que el proceso se reinicia.
  • Crear cursores en un bucle sin cerrarlos. Cada cursor abierto contiene su conjunto de resultados en memoria.

Optimización de índices y planes de consulta

Comprobar el rendimiento de consultas en el lado del servidor

Usa SET STATISTICS TIME ON y SET STATISTICS IO ON para ver cuánto tardan las consultas en el servidor y cuántos datos leen. Las lecturas lógicas altas suelen indicar que falta un índice. Ejecuta estas sentencias en SQL Server Management Studio o en la extensión MSSQL para Visual Studio Code, donde la salida aparece en el panel de Mensajes:

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT * FROM Production.Product WHERE ProductSubcategoryID = 1;

SET STATISTICS TIME OFF;
SET STATISTICS IO OFF;

Debería ver una salida similar a la siguiente:

Table 'Product'. Scan count 1, logical reads 3
SQL Server Execution Times: CPU time = 0 ms, elapsed time = 1 ms.

Si ves lecturas lógicas altas o escaneos de tablas, considera añadir un índice.

Usa las pistas de consulta como solución táctica

Las sugerencias de consulta anulan las decisiones del optimizador de consultas sobre los índices y la estrategia de combinación. En producción, son valiosos como un parche rápido y de bajo riesgo cuando una consulta sufre de repente una regresión. Puedes desplegar el hint en el código de tu aplicación inmediatamente para estabilizar la consulta mientras investigas la causa raíz (índices faltantes, estadísticas obsoletas o cambios en el esquema).

Evita dejar las indicaciones de forma permanente. Cuando la distribución de datos o el esquema cambia, una pista codificada puede empeorar las cosas. Trátalos como temporales y vuelve a revisarlos una vez que se resuelva el problema subyacente:

cursor.execute("""
    SELECT * FROM Production.Product WITH (INDEX(AK_Product_Name))
    WHERE ProductSubcategoryID = %(subcategory_id)s
""", {"subcategory_id": 1})

Usa OPTION (RECOMPILE) para evitar planes en caché defectuosos

SQL Server almacena en caché los planes de consulta basándose en el primer conjunto de valores de parámetros que detecta. Si la distribución de datos varía mucho entre llamadas, el plan en caché puede rendir mal para algunos valores. Este problema, llamado detección de parámetros, suele aparecer como una consulta que "antes era rápida" que de repente tarda segundos o minutos.

OPTION (RECOMPILE)obliga a SQL Server a construir un plan nuevo para cada ejecución, lo cual es una solución inmediata y efectiva que puedes desplegar sin ningún cambio en el lado del servidor. La contrapartida es un pequeño coste de compilación por llamada, pero para las consultas que se ejecutan con poca frecuencia o devuelven conjuntos de resultados de tamaño variable, ese coste es insignificante en comparación con ejecutar un plan deficiente.

Una vez que hayas estabilizado el problema, puedes tomarte tu tiempo aplicando una solución permanente, como reescribir la consulta, añadir índices filtrados o usar guías de planos:

cursor.execute("""
    SELECT * FROM Sales.SalesOrderHeader
    WHERE OrderDate > %(start_date)s
    OPTION (RECOMPILE)
""", {"start_date": start_date})

Supervisión del rendimiento

Cronometra tus consultas

Para encontrar operaciones lentas, envuelva las consultas con time.perf_counter():

import time

start = time.perf_counter()
cursor.execute("SELECT SalesOrderID, OrderDate, TotalDue FROM Sales.SalesOrderHeader WHERE CustomerID = %(customer_id)s", {"customer_id": customer_id})
rows = cursor.fetchall()
elapsed = time.perf_counter() - start

print(f"Query returned {len(rows)} rows in {elapsed:.3f}s")

Para una visión más amplia de dónde pasa tu aplicación, utiliza el módulo integrado cProfile de Python:

python -m cProfile -s cumtime my_app.py

Esta vista muestra el tiempo acumulado por llamada a la función, lo que ayuda a identificar si la lentitud está en la ejecución de la consulta, el procesamiento de datos o la latencia de red.

Utiliza Almacén de consultas para análisis en el lado del servidor

El temporizador del lado del cliente te indica cuánto tarda una consulta desde la perspectiva de tu aplicación, pero combina la latencia de red, el tiempo de ejecución del servidor y el procesamiento del cliente. Almacén de consultas captura planes de ejecución y estadísticas de ejecución en el servidor, para que puedas ver exactamente cómo SQL Server ejecutó cada consulta, con qué frecuencia se ejecutó y cómo cambió su rendimiento con el tiempo.

Almacén de consultas es especialmente útil para identificar la captura de parámetros, las regresiones en los planes de ejecución y las consultas que más recursos del servidor consumen. Puedes consultar directamente las vistas sys.query_store_runtime_stats y sys.query_store_plan, o usar los informes integrados de Almacén de consultas en SQL Server Management Studio.

Utilizar los informes del Panel de Rendimiento

Los informes del Panel de Rendimiento en SQL Server Management Studio ofrecen una visión general en tiempo real del estado de estado de SQL Server, incluyendo tipos de espera actuales, consultas activas y costosas y tendencias de CPU/IO. Úsalos para detectar rápidamente cuellos de botella sin presentar consultas directamente a las DMV.

Lista de comprobación de rendimiento

Conexión

  • [ ] Habilitar la agrupación de conexiones.
  • [ ] Dimensiona el grupo para tu carga de trabajo.
  • [ ] Reutilizar conexiones dentro de las operaciones.
  • [ ] Mantener abiertas las conexiones en servicios de larga duración.

Queries

  • [ ] Selecciona solo las columnas que necesites.
  • [ ] Utiliza el método de búsqueda adecuado para cada consulta.
  • [ ] Implementar paginación en el servidor.
  • [ ] Configura SET NOCOUNT ON una vez después de conectarlo.
  • [ ] Minimiza los viajes de ida y vuelta agrupando consultas.

Inserciones

  • [ ] Uso execute() para insertos de una sola fila.
  • [ ] Úsalos executemany() para lotes pequeños o moderados (~10-1.000 hileras).
  • [ ] Úsalo bulkcopy() cuando el rendimiento importa más que el control por fila.

Almacenamiento en memoria caché

  • [ ] Datos de referencia en caché con TTL para evitar que se sirvan resultados obsoletos.

Resources

  • [ ] Procesa resultados grandes en bloques o con generadores.
  • [ ] Limpia las conexiones cuanto antes.
  • [ ] Monitoriza con tracemalloc el uso de memoria en servicios de larga duración.