Penyetelan performa untuk aplikasi mssql-python

Driver mssql-python menyediakan beberapa fitur dan pola untuk mengoptimalkan performa aplikasi SQL Server, termasuk pengumpulan koneksi, pengoptimalan kueri, dan operasi massal.

Manajemen koneksi

Gunakan pengumpulan koneksi

Pengumpulan koneksi sudah terpasang. Saat Anda memanggil conn.close(), koneksi dikembalikan ke pool untuk digunakan kembali alih-alih dihancurkan, sehingga pemanggilan connect() berikutnya tidak perlu melakukan handshake yang memakan banyak sumber daya:

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

Mengonfigurasi ukuran kumpulan untuk beban kerja

Sesuaikan ukuran kumpulan berdasarkan persyaratan konkurensi Anda. Jika aplikasi Anda menangani banyak pengguna secara simultan, tingkatkan kumpulan. Untuk beban kerja yang lebih ringan, kumpulan yang lebih kecil menghemat sumber daya server:

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
)

Gunakan kembali koneksi dalam operasi

Membuka koneksi baru untuk setiap kueri menambahkan overhead bahkan dengan pengumpulan. Sebagai gantinya, tahan satu koneksi selama durasi operasi logis:

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

Jaga agar koneksi tetap terbuka di layanan yang berjalan lama

Server web, pekerja antrean, dan tugas terjadwal yang berjalan terus-menerus sebaiknya mempertahankan koneksi tetap terbuka daripada membuka dan menutup koneksi untuk setiap operasi. Membuka koneksi melibatkan jabat tangan TCP, negosiasi TLS, dan otentikasi, yang dapat memakan waktu 50-200 ms tergantung pada jarak jaringan dan metode autentikasi. Untuk pekerja antrean yang memproses ribuan pesan per jam, overhead itu bertambah dengan cepat.

Jaga agar koneksi tetap terbuka selama masa pakai pekerja dan sambungkan kembali saat koneksi gagal. Tidur di antara iterasi untuk menghindari memukul server saat antrean kosong:

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

Dengan pengumpulan koneksi diaktifkan (default), kumpulan menangani koneksi menganggur untuk Anda. Tetapi jika Anda menonaktifkan pengumpulan atau menggunakan satu koneksi khusus, atur Connection Timeout dan Command Timeout di string koneksi Anda untuk mendeteksi koneksi kedaluwarsa lebih awal daripada macet.

Pengoptimalan kueri

Mengambil hanya data yang diperlukan

Memilih hanya kolom yang digunakan aplikasi Anda mengurangi transfer jaringan, konsumsi memori, dan waktu eksekusi kueri.

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

Gunakan metode fetch yang sesuai

Driver menyediakan beberapa metode pengambilan data. Gunakan salah satu yang cocok dengan ukuran hasil Anda:

  • fetchval() mengembalikan satu nilai skalar dengan overhead minimal.
  • fetchall() memuat seluruh kumpulan hasil ke dalam memori, yang berfungsi dengan baik untuk tabel kecil.
  • fetchmany(n) mengambil baris secara bertahap, sehingga penggunaan memori tetap konstan untuk kumpulan hasil yang besar.

Ukuran batch yang tepat bergantung pada lebar baris fetchmany(). Untuk baris sempit (beberapa kolom kecil, masing-masing kira-kira 1 KB), 1.000 baris menyimpan setiap batch sekitar 1 MB memori. Untuk baris yang lebih lebar dengan string besar atau kolom biner, gunakan ukuran batch yang lebih kecil. Mulailah dengan 1.000 dan sesuaikan berdasarkan data Anda.

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)

Gunakan paginasi server-side

Alih-alih mengambil semua baris dan mengiris di Python, gunakan OFFSET/FETCH NEXT untuk mengambil hanya halaman yang Anda butuhkan.

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

Gunakan SET NOCOUNT ON

Secara default, SQL Server mengirimkan pesan "baris yang terpengaruh" setelah setiap pernyataan DML. SET NOCOUNT ON menyembunyikan pesan-pesan ini dan mengurangi lalu lintas jaringan. Ini adalah pengaturan tingkat sesi, jadi atur sekali setelah tersambung daripada menyematkannya di setiap kueri.

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

Pilih metode sisipan yang tepat

Driver menyediakan tiga cara untuk menyisipkan data, masing-masing sesuai dengan skala yang berbeda:

Metode Jumlah baris Mengapa
execute() 1 baris setiap panggilan Gunakan untuk operasi baris tunggal seperti pengiriman formulir atau penangan API di mana Anda memerlukan ID yang dimasukkan segera.
executemany() ~10 - 1.000 baris Menggunakan pengikatan parameter berdasarkan kolom untuk throughput yang lebih baik daripada loop. Mengirim setiap baris sebagai pernyataan berparameter.
bulkcopy() 100 baris atau lebih Menggunakan protokol penyisipan massal TDS, yang jauh lebih efisien daripada penyisipan baris demi baris. Terbaik untuk pemuatan data, migrasi, dan pemrosesan batch.

Untuk detail dan contoh selengkapnya, lihat Pola pemuatan dan pergerakan data.

Penyisipan tunggal dengan execute()

Gunakan untuk sisipan satu kali di mana Anda membutuhkan hasilnya segera. Production.Product memiliki beberapa kolom NOT NULL tanpa nilai bawaan, jadi pernyataan INSERT mencantumkan semuanya:

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

Penyisipan batch dengan executemany()

executemany() mengikat parameter berdasarkan kolom dan mengirimkannya secara efisien. Gunakan ini untuk batch berukuran sedang alih-alih memanggil execute() dalam perulangan. Perhatikan bahwa executemany() memerlukan penanda posisi ? dengan daftar tuple, sementara execute() mendukung parameter bernama ?%(name)s dengan dikte. Lihat Kueri berparameter untuk detail tentang setiap gaya.

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

Salinan massal untuk beban besar

Jika throughput lebih penting daripada kontrol per baris, beralihlah ke bulkcopy(). Ini mengalirkan baris data melalui protokol penyisipan massal TDS dan menghindari overhead per baris pada pernyataan berparameter. Titik peralihan yang tepat saat bulkcopy() mengungguli executemany() bergantung pada lebar baris dan latensi jaringan, tetapi biasanya berada pada kisaran seratusan baris. Untuk batch yang sangat kecil, executemany() lebih sederhana karena bulkcopy() membuat koneksi internal terpisah dan melakukan secara otomatis.

Tidak seperti execute() dan executemany(), bulkcopy() memetakan nilai ke kolom berdasarkan posisi, bukan dengan INSERT daftar kolom. Berikan column_mappings untuk menentukan nama kolom tujuan tempat data dimuat, sehingga tuple pada sumber sejajar dengan kolom yang benar, bukan dengan kolom identitas pertama pada tabel:

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

Untuk beban yang sangat besar, gunakan generator untuk menghindari pemuatan seluruh himpunan data ke dalam memori dan atur batch_size untuk melakukan secara berkala:

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

Strategi tembolok

Untuk data referensi yang jarang berubah (kategori, tabel pencarian, konfigurasi), cache hasil dalam aplikasi Anda alih-alih mengkueri setiap permintaan.

functools.lru_cache Python menyediakan memoisasi sederhana, tetapi cache-nya disimpan terus hingga proses dimulai ulang. Jika data yang mendasarinya dapat berubah, gunakan cachetools.TTLCache untuk me-refresh secara otomatis setelah batas waktu:

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

Pengoptimalan jaringan

Minimalkan perjalanan pulang pergi

Setiap kueri memerlukan satu perjalanan pulang-pergi melalui jaringan ke server. Gabungkan kueri terkait ke dalam satu batch dan gunakan nextset() untuk maju melalui kumpulan hasil:

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

Gunakan pemrosesan sisi server untuk logika yang kompleks

Dorong agregasi dan pemfilteran ke SQL Server alih-alih mengambil baris mentah dan memprosesnya dalam Python. Server mengembalikan satu baris ringkasan, bukan berpotensi ribuan baris detail:

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

Hindari operasi kursor yang berselang-seling

Driver mssql-python tidak mendukung Beberapa Set Hasil Aktif (MARS). Hanya satu kursor yang dapat memiliki kueri aktif per koneksi. Ambil kumpulan hasil pertama sepenuhnya sebelum menjalankan kueri berikutnya, atau gunakan koneksi kedua:

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

Manajemen memori

Memproses hasil besar dalam potongan

Memuat tabel jutaan baris ke dalam daftar menghabiskan memori sebanding dengan kumpulan hasil penuh. Gunakan OFFSET dan FETCH NEXT untuk melakukan paginasi data di sisi server dan memproses satu chunk data pada satu waktu.

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
)

Menggunakan generator untuk streaming

Pembungkus fetchmany() generator Python menjaga penggunaan memori tetap konstan terlepas dari ukuran tabel. Pemanggil mengulangi baris demi baris tanpa memuat kumpulan hasil lengkap. Untuk sumber ekstra besar, gabungkan tabel dengan UNION ALL dan streaming hasil gabungan dengan cara yang sama.

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

Bersihkan sumber daya dengan segera

Koneksi yang tidak tertutup mengikat sumber daya server dan dapat menghabiskan kumpulan koneksi. Gunakan pengelola konteks untuk menjamin pembersihan bahkan ketika pengecualian terjadi.

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

Pantau penggunaan memori

Set hasil yang besar, cache berumur panjang, dan objek koneksi semuanya menghabiskan memori. Jika aplikasi Anda berjalan sebagai layanan, kebocoran memori akibat kursor yang tidak ditutup atau cache yang tidak dibatasi pada akhirnya dapat menyebabkan proses diakhiri oleh OS atau runtime kontainer.

Gunakan modul Python tracemalloc untuk menjepret memori dan menemukan alokasi terbesar.

import tracemalloc

tracemalloc.start()

# ... run your workload ...

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

Sumber umum pertumbuhan memori yang tidak terduga meliputi:

  • Memanggil fetchall() pada kueri yang mengembalikan jutaan baris. Gunakan fetchmany() atau generator sebagai gantinya.
  • Menyimpan hasil kueri dalam cache tanpa maxsize atau TTL. Cache akan terus bertambah sampai proses dimulai ulang.
  • Membuat kursor dalam loop tanpa menutupnya. Setiap kursor yang terbuka menyimpan kumpulan hasilnya dalam memori.

Pengoptimalan rencana indeks dan kueri

Periksa kinerja kueri pada sisi server

Gunakan SET STATISTICS TIME ON dan SET STATISTICS IO ON untuk melihat berapa lama kueri yang dibutuhkan di server dan berapa banyak data yang mereka baca. Pembacaan logikal yang tinggi biasanya menunjukkan adanya indeks yang hilang. Jalankan pernyataan ini di SQL Server Management Studio atau ekstensi MSSQL untuk Visual Studio Code, di mana output muncul di panel Pesan:

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT * FROM Production.Product WHERE ProductSubcategoryID = 1;

SET STATISTICS TIME OFF;
SET STATISTICS IO OFF;

Anda akan melihat output seperti:

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

Jika Anda melihat pembacaan logis tinggi atau pemindaian tabel, pertimbangkan untuk menambahkan indeks.

Gunakan petunjuk kueri sebagai solusi taktis

Petunjuk kueri menggantikan indeks pengoptimal kueri dan pilihan strategi gabungan. Dalam produksi, hal ini berguna sebagai tambalan cepat yang berisiko rendah ketika kueri tiba-tiba mengalami penurunan kinerja. Anda dapat segera menyebarkan petunjuk dalam kode aplikasi Anda untuk menstabilkan kueri saat Anda menyelidiki akar penyebab (indeks yang hilang, statistik kedaluwarsa, atau perubahan skema).

Hindari meninggalkan petunjuk di tempatnya secara permanen. Ketika distribusi data atau skema berubah, petunjuk hardcode dapat memperburuk keadaan. Perlakukan mereka sebagai sementara dan tinjau kembali setelah masalah yang mendasarinya diselesaikan:

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

Gunakan OPTION (RECOMPILE) untuk melewati paket cache yang buruk

SQL Server menyimpan paket kueri dalam cache berdasarkan kumpulan nilai parameter pertama yang dilihatnya. Jika distribusi data sangat bervariasi antar panggilan, paket yang di-cache mungkin berkinerja buruk untuk beberapa nilai. Masalah ini, yang disebut parameter sniffing, sering muncul sebagai kueri yang "dulunya cepat" tiba-tiba memakan waktu beberapa detik atau menit.

OPTION (RECOMPILE)memaksa SQL Server untuk membangun rencana baru untuk setiap eksekusi, yang merupakan perbaikan langsung yang efektif yang dapat Anda terapkan tanpa perubahan sisi server. Komprominya adalah biaya kompilasi kecil untuk setiap pemanggilan, tetapi untuk kueri yang jarang dijalankan atau mengembalikan kumpulan hasil dengan ukuran yang bervariasi, biaya tersebut dapat diabaikan dibandingkan dengan menjalankan rencana eksekusi yang buruk.

Setelah menstabilkan masalah, Anda dapat meluangkan waktu untuk menerapkan perbaikan permanen seperti menulis ulang kueri, menambahkan indeks yang difilter, atau menggunakan panduan paket:

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

Pemantauan kinerja

Atur waktu pertanyaan Anda

Untuk menemukan operasi yang lambat, bungkus kueri dengan 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")

Untuk tampilan yang lebih luas tentang tempat aplikasi Anda menghabiskan waktu, gunakan modul bawaan cProfile Python:

python -m cProfile -s cumtime my_app.py

Tampilan ini menunjukkan waktu kumulatif per panggilan fungsi, yang membantu Anda mengidentifikasi apakah kelambatan dalam eksekusi kueri, pemrosesan data, atau latensi jaringan.

Menggunakan Query Store untuk analisis sisi server

Pengaturan waktu di sisi klien menunjukkan berapa lama kueri berlangsung dari sudut pandang aplikasi Anda, tetapi mencakup latensi jaringan, waktu eksekusi server, dan pemrosesan klien. Query Store menangkap rencana eksekusi dan statistik runtime di server, sehingga Anda dapat melihat dengan tepat bagaimana SQL Server mengeksekusi setiap kueri, seberapa sering berjalan, dan bagaimana performanya berubah dari waktu ke waktu.

Query Store sangat berguna untuk mengidentifikasi pengendusan parameter, regresi rencana, dan kueri yang menghabiskan sumber daya server paling banyak. Anda dapat membuat kueri pada view sys.query_store_runtime_stats dan sys.query_store_plan tersebut secara langsung, atau menggunakan laporan Query Store bawaan di SQL Server Management Studio.

Menggunakan Laporan Dasbor Performa

Laporan Dasbor Performa di SQL Server Management Studio memberikan gambaran umum real-time tentang kesehatan SQL Server, termasuk jenis tunggu saat ini, kueri mahal aktif, dan tren CPU/IO. Gunakan fitur ini untuk dengan cepat mengidentifikasi hambatan tanpa menulis kueri langsung pada DMV.

Daftar Periksa Kinerja

Koneksi

  • [ ] Aktifkan pengumpulan koneksi.
  • [ ] Tentukan ukuran pool sesuai beban kerja Anda.
  • [ ] Gunakan kembali koneksi dalam operasi.
  • [ ] Jaga agar koneksi tetap terbuka di layanan yang berjalan lama.

Queries

  • [ ] Pilih hanya kolom yang Anda butuhkan.
  • [ ] Gunakan metode pengambilan yang sesuai untuk setiap kueri.
  • [ ] Terapkan paginasi sisi server.
  • [ ] Atur SET NOCOUNT ON sekali setelah terhubung.
  • [ ] Minimalkan round trip dengan mengelompokkan kueri secara batch.

Sisipan

  • [ ] Gunakan execute() untuk sisipan baris tunggal.
  • [ ] Gunakan executemany() untuk batch kecil hingga sedang (~10-1.000 baris).
  • [ ] Gunakan bulkcopy() saat throughput lebih penting daripada kontrol per baris.

Penggunaan Cache

  • [ ] Simpan data referensi dengan TTL agar tidak menyajikan hasil usang.

Resources

  • [ ] Proses hasil besar dalam potongan atau dengan generator.
  • [ ] Segera bersihkan sambungan.
  • [ ] Pantau penggunaan memori pada layanan yang berjalan dalam jangka waktu lama dengan tracemalloc.