Prestatie-tuning voor mssql-python-applicaties

De mssql-python-driver biedt verschillende functies en patronen om de prestaties van SQL Server-applicaties te optimaliseren, waaronder verbindingspooling, queryoptimalisatie en bulkoperaties.

Verbindingsbeheer

Groepsgewijze verbinding gebruiken

Verbindingspooling is ingebouwd. Wanneer je belt conn.close(), keert de verbinding terug naar de pool voor hergebruik in plaats van vernietigd te worden, dus latere connect() oproepen slaan de dure handdruk over:

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()

Poolgrootte configureren voor workload

Pas de poolgrootte aan op basis van je concurrency-eisen. Als je applicatie veel gelijktijdige gebruikers verwerkt, vergroot dan de pool. Voor lichtere workloads bespaart een kleinere pool serverresources:

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
)

Verbindingen in bewerkingen hergebruiken

Het openen van een nieuwe verbinding voor elke query voegt overhead toe, zelfs met pooling. Houd in plaats daarvan één verbinding vast gedurende de duur van een logische bewerking:

# 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()

Houd verbindingen open in langdurige diensten

Webservers, wachtrijprocessen en geplande taken die continu draaien, zouden de verbindingen open moeten houden in plaats van bij elke bewerking opnieuw verbinding te maken en te verbreken. Het openen van een verbinding vereist een TCP-handshake, TLS-onderhandeling en authenticatie, wat 50-200 ms kan duren afhankelijk van netwerkafstand en authenticatiemethode. Voor een wachtrijmedewerker die duizenden berichten per uur verwerkt, loopt die overhead snel op.

Houd de verbinding open gedurende de levensduur van de werknemer en maak opnieuw verbinding wanneer de verbinding uitvalt. Slaap tussen iteraties om te voorkomen dat de server wordt overbelast als de wachtrij leeg is:

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()

Met connection pooling ingeschakeld (de standaard) regelt de pool de idle verbindingen voor je. Maar als je pooling uitschakelt of één speciale verbinding gebruikt, stel Connection TimeoutCommand Timeout dan in je verbindingsreeks in om verouderde verbindingen vroeg te detecteren in plaats van te hangen.

Queryoptimalisatie

Haal alleen de benodigde gegevens op

Het selecteren van alleen de kolommen die je applicatie gebruikt, vermindert netwerkoverdracht, geheugenverbruik en query-uitvoeringstijd.

# 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})

Gebruik geschikte fetch-methoden

De driver biedt verschillende fetch-methoden. Gebruik degene die overeenkomt met de grootte van je resultaat:

  • fetchval() geeft één scalaire waarde terug met minimale overhead.
  • fetchall() laadt de volledige resultaatset in het geheugen, wat goed werkt voor kleine tabellen.
  • fetchmany(n) haalt rijen in batches op, waarbij het geheugengebruik constant blijft voor grote resultaatsets.

De juiste batchgrootte voor fetchmany() hangt af van de rijbreedte. Voor smalle rijen (een paar kleine kolommen, ongeveer 1 KB elk) houdt 1.000 rijen elke batch rond de 1 MB geheugen. Voor bredere rijen met grote strings of binaire kolommen gebruik een kleinere batchgrootte. Begin met 1.000 en pas aan op basis van je data.

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)

Gebruik server-side paginering

In plaats van alle rijen op te halen en in Python op te delen, gebruik OFFSET/FETCH NEXT om alleen de pagina op te halen die je nodig hebt.

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()

Gebruik SET NOCOUNT ON

Standaard stuurt SQL Server na elke DML-instructie een bericht "rows affected". SET NOCOUNT ON onderdrukt deze berichten en vermindert het netwerkverkeer. Het is een sessie-niveau setting, dus stel het één keer in na het verbinden in plaats van het in elke query in te embeddingen.

# 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"}
)

Kies de juiste invoegmethode

De driver biedt drie manieren om data in te voegen, elk geschikt voor een andere schaal:

Methode Aantal rijen Waarom
execute() 1 rij per oproep Gebruik deze voor single-row operaties zoals formulierindiening of API-handlers waarbij je de ingevoerde ID direct nodig hebt.
executemany() ~10-1.000 rijen Gebruikt kolomgewijze parameterbinding voor betere doorvoer dan een lus. Stuurt elke rij als een geparametriseerde instructie.
bulkcopy() Honderden rijen of meer Gebruikt het TDS bulk insert-protocol, dat aanzienlijk efficiënter is dan row-by-row inserts. Het beste voor dataloads, migraties en batchverwerking.

Voor meer details en voorbeelden, zie Data loading and movement patterns.

Afzonderlijke invoegingen met execute()

Gebruik het voor eenmalige inzetstukken waarbij je het resultaat direct nodig hebt. Production.Product heeft meerdere NIET NULL-kolommen zonder standaardinstellingen, dus de insert vermeldt ze allemaal:

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()

Batchinvoegingen met executemany()

executemany() bindt parameters kolomgewijs en stuurt deze efficiënt. Gebruik dit voor middelgrote batches in plaats van execute() in een lus aan te roepen. Let op dat executemany() positionele ? markers vereist met een lijst van tuples, terwijl execute() zowel ? als benoemde %(name)s parameters met dicts ondersteunt. Zie Geparametriseerde queries voor details over elke stijl.

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()

In bulk kopiëren voor grote hoeveelheden

Wanneer doorvoer belangrijker is dan controle per rij, schakel dan over naar bulkcopy(). Het streamt rijen door het TDS bulk insert-protocol en voorkomt de per-rij overhead van geparametriseerde statements. Het exacte omslagpunt waarop bulkcopy() beter presteert dan executemany() hangt af van de rijbreedte en netwerklatentie, maar ligt doorgaans in de lage honderden. Voor zeer kleine batches is executemany() eenvoudiger, omdat bulkcopy() een afzonderlijke interne verbinding maakt en automatisch vastlegt.

In tegenstelling tot execute() en executemany(), bulkcopy() kaarten waarden af aan kolommen op basis van positie, niet op basis van een INSERT kolomlijst. Geef column_mappings op om de doelkolommen op te geven waarin je laadt, zodat de brontuples overeenkomen met de juiste kolommen in plaats van met de eerste identiteitskolom van de tabel:

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

Voor zeer grote belastingen gebruik je een generator om te voorkomen dat je de hele dataset in het geheugen laadt en stel je batch_size in om periodiek te committen:

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,
)

Cache-strategieën

Voor referentiegegevens die zelden veranderen (categorieën, opzoektabellen, configuratie), cache de resultaten in je applicatie in plaats van bij elke aanvraag te queryen.

Python functools.lru_cache biedt eenvoudige memoisatie, maar het wordt onbeperkt gecached totdat het proces opnieuw wordt gestart. Als de onderliggende gegevens kunnen veranderen, gebruik cachetools.TTLCache om na een bepaalde tijd automatisch te vernieuwen:

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()

Netwerkoptimalisatie

Minimaliseer retourreizen

Elke aanvraag is een heen-en-terugreis via het netwerk naar de server. Combineer gerelateerde queries tot één batch en gebruik nextset() deze om door de resultaatsets te gaan:

# 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()

Gebruik server-side verwerking voor complexe logica

Stuur aggregatie en filtering naar SQL Server in plaats van ruwe rijen op te halen en ze in Python te verwerken. De server geeft een enkele samenvattingsrij terug in plaats van mogelijk duizenden detailrijen:

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})

Vermijd door elkaar lopende cursorbewerkingen

De mssql-python-driver ondersteunt geen Multiple Active Result Sets (MARS). Er kan per verbinding slechts één cursor een actieve query hebben. Haal de eerste resultaatset volledig op voordat je de volgende query uitvoert, of gebruik een tweede verbinding:

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()

Geheugenbeheer

Verwerk grote resultaten in blokken

Het laden van een tabel met meerdere miljoenen rijen in een lijst verbruikt geheugen evenredig aan de volledige resultaatset. Gebruik OFFSET en FETCH NEXT om de gegevens aan de serverzijde te pagineren en steeds één deel tegelijk te verwerken.

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
)

Gebruik generatoren voor streaming

Een Python-generatorwrapping fetchmany() houdt het geheugengebruik constant ongeacht de tabelgrootte. De beller herhaalt rij voor rij zonder de volledige resultaatset te laden. Voor een extra grote bron combineer je tabellen met UNION ALL en stream je het gecombineerde resultaat op dezelfde manier.

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")

Ruim de middelen snel op

Ongesloten verbindingen binden serverbronnen en kunnen de verbindingspool uitputten. Gebruik een contextmanager om opruiming te garanderen, zelfs als er uitzonderingen optreden.

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()

Geheugengebruik bewaken

Grote resultaatsets, langlevende caches en verbindingsobjecten verbruiken allemaal geheugen. Als je applicatie als service draait, kunnen geheugenlekken van ongesloten cursors of onbegrensde caches uiteindelijk ertoe leiden dat het proces wordt gedood door het besturingssysteem of de containerruntime.

Gebruik de tracemalloc module van Python om geheugen te snapshoten en de grootste allocaties te vinden.

import tracemalloc

tracemalloc.start()

# ... run your workload ...

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

Veelvoorkomende bronnen van onverwachte geheugengroei zijn:

  • fetchall() aanroepen op een query die miljoenen rijen retourneert. Gebruik fetchmany() of in plaats daarvan een generator.
  • Caching van zoekresultaten zonder een maxsize of TTL. Caches groeien totdat het proces opnieuw wordt gestart.
  • Cursoren aanmaken in een lus zonder ze te sluiten. Elke open cursor bewaart zijn resultaatset in het geheugen.

Optimalisatie van indexen en queryplannen

Controleer de prestaties van query's aan de serverzijde

Gebruik SET STATISTICS TIME ON en SET STATISTICS IO ON om te zien hoe lang queries op de server duren en hoeveel data ze lezen. Hoge aantallen logische leesbewerkingen wijzen meestal op een ontbrekende index. Voer deze instructies uit in SQL Server Management Studio of de MSSQL-extensie voor Visual Studio Code, waar de output verschijnt in het Berichten-paneel:

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT * FROM Production.Product WHERE ProductSubcategoryID = 1;

SET STATISTICS TIME OFF;
SET STATISTICS IO OFF;

U zou uitvoer moeten zien zoals:

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

Als je hoge logische reads of tabelscans ziet, overweeg dan een index toe te voegen.

Gebruik query hints als een tactische oplossing

Query hints overschrijven de keuzes van de query-optimizer voor indexen en joinstrategieën. In een productieomgeving zijn ze waardevol als een snelle oplossing met weinig risico wanneer een query opeens slechter presteert. Je kunt de hint direct in je applicatiecode inzetten om de query te stabiliseren terwijl je de oorzaak onderzoekt (ontbrekende indexen, verouderde statistieken of schemawijzigingen).

Vermijd om hints permanent te laten staan. Wanneer de dataverdeling of het schema verandert, kan een hardcoded hint het probleem verergeren. Behandel ze als tijdelijk en bekijk ze opnieuw nadat het onderliggende probleem is opgelost:

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

Gebruik OPTION (RECOMPILE) om slechte gecachede plannen te omzeilen

SQL Server cachet queryplannen op basis van de eerste set parameters die het ziet. Als de gegevensverdeling sterk varieert tussen aanroepen, kan het gecachte plan voor sommige waarden slecht presteren. Dit probleem, parameter sniffing genoemd, duikt vaak op als een query die "vroeger snel was" en plotseling seconden of minuten duurt.

OPTION (RECOMPILE)dwingt SQL Server om voor elke uitvoering een nieuw plan te bouwen, wat een effectieve directe oplossing is die je kunt implementeren zonder server-side wijzigingen. Het nadeel is dat er per aanroep enige compilatie-overhead is, maar voor query’s die zelden worden uitgevoerd of resultaatsets van wisselende grootte retourneren, zijn die kosten verwaarloosbaar vergeleken met de gevolgen van een slecht uitvoeringsplan.

Zodra je het probleem hebt gestabiliseerd, kun je de tijd nemen om een permanente oplossing toe te passen, zoals het herschrijven van de query, het toevoegen van gefilterde indexen of het gebruik van plangidsen:

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

Prestatiebewaking

Tijd je zoekopdrachten

Om langzame operaties te vinden, wikkel je queries in met 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")

Voor een breder beeld van waar je applicatie tijd doorbrengt, gebruik de ingebouwde cProfile module van Python:

python -m cProfile -s cumtime my_app.py

Deze weergave toont de cumulatieve tijd per functieaanroep, wat helpt te bepalen of traagheid zit in query-uitvoering, dataverwerking of netwerklatentie.

Gebruik Query Store voor server-side analyse

Client-side timing geeft aan hoe lang een query duurt vanuit het perspectief van je applicatie, maar combineert netwerklatentie, serveruitvoeringstijd en clientverwerking. Query Store legt uitvoeringsplannen en runtime-statistieken op de server vast, zodat je precies kunt zien hoe SQL Server elke query uitvoerde, hoe vaak deze werd uitgevoerd en hoe de prestaties in de loop van de tijd veranderden.

Query Store is vooral nuttig voor het identificeren van parametersniffing, planregressies en queries die de meeste serverbronnen verbruiken. Je kunt direct de sys.query_store_runtime_stats en sys.query_store_plan weergaven opvragen, of de ingebouwde Query Store-rapporten in SQL Server Management Studio gebruiken.

Gebruik Performance Dashboard-rapporten

De Performance Dashboard-rapporten in SQL Server Management Studio bieden een realtime overzicht van de gezondheid van SQL Server, inclusief huidige wachttypen, actieve dure queries en CPU/IO-trends. Gebruik ze om snel knelpunten te ontdekken zonder rechtstreeks query's op de DMV's te schrijven.

Controlelijst voor prestaties

Verbinding

  • [ ] Schakel verbindingspooling in.
  • [ ] Stem de grootte van de pool af op je werklast.
  • [ ] Hergebruik verbindingen binnen operaties.
  • [ ] Houd verbindingen open bij langlopende diensten.

Queries

  • [ ] Selecteer alleen de kolommen die je nodig hebt.
  • [ ] Gebruik de juiste fetch-methode voor elke query.
  • [ ] Implementeer server-side paginering.
  • [ ] Stel SET NOCOUNT ON eenmaal in nadat u verbinding hebt gemaakt.
  • [ ] Beperk het aantal heen-en-weeraanvragen door query’s te bundelen.

Invoegingen

  • [ ] Gebruik execute() voor inzetstukken met één rij.
  • [ ] Gebruik executemany() voor kleine tot matige batches (~10-1.000 rijen).
  • [ ] Gebruik bulkcopy() wanneer doorvoer belangrijker is dan controle per rij.

Cachebeheer

  • [ ] Cache referentiegegevens met een TTL om te voorkomen dat verouderde resultaten worden geleverd.

Resources

  • [ ] Verwerk grote resultaten in stukken of met generatoren.
  • [ ] Maak de verbindingen snel schoon.
  • [ ] Monitor het geheugengebruik met tracemalloc in langlopende diensten.